Understanding SOLID Principles of object-oriented software design using Python

Neeraj Kushwaha
8 min readFeb 20, 2023

--

This is the 16th post in a series of learning the Python programming language.

SOLID is an acronym that represents five principles of object-oriented software design. These principles provide guidelines for creating maintainable and scalable software systems. SOLID principles were introduced by Robert C. Martin and have become a standard in software development.

The SOLID principles are:

  1. Single Responsibility Principle (SRP)
  2. Open-Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

Let’s go through each of the SOLID principles and see how they can be applied to Python.

Single Responsibility Principle (SRP)

The Single Responsibility Principle (SRP) is a concept in software development that states that every module, class, or function should have only one reason to change. In other words, it should have only one responsibility.

In practice, this means that a class should have only one job and do it well. If a class has multiple responsibilities, it becomes more complex, difficult to maintain, and prone to breaking when changes are made. This can make it hard to understand what the class is doing, making it difficult to debug or add new features.

Let’s see an example of how to implement the SRP in Python:

Suppose we have a class called FileProcessor that is responsible for reading data from a file, processing the data, and writing the processed data back to the file. This class violates the SRP, as it has multiple responsibilities. To fix this, we can split the class into two separate classes: FileReader and DataProcessor.

class FileReader:
def __init__(self, file_path):
self.file_path = file_path

def read_file(self):
with open(self.file_path, 'r') as file:
return file.read()

class DataProcessor:
def process_data(self, data):
# processing logic goes here
return processed_data

class FileProcessor:
def __init__(self, file_path):
self.file_reader = FileReader(file_path)
self.data_processor = DataProcessor()

def process_file(self):
data = self.file_reader.read_file()
processed_data = self.data_processor.process_data(data)
# write processed data back to file

In this example, the FileReader class is responsible for reading data from the file, and the DataProcessor class is responsible for processing the data. The FileProcessor class is now responsible for tying everything together, and its only responsibility is to process the file.

By splitting the responsibilities into separate classes, we have made our code more maintainable and easier to understand. If we need to change the way data is processed, we can simply modify the DataProcessor class, without having to touch the FileReader or FileProcessor classes. Additionally, if we need to change the way data is read from the file, we can modify the FileReader class, without affecting the rest of the code.

The Single Responsibility Principle is a crucial concept in software development that helps ensure that code is maintainable, scalable, and easy to understand. By following the SRP, we can write better, more modular code that is easier to debug, test, and extend.

Open-Closed Principle (OCP)

The Open-Closed Principle (OCP) states that software entities (such as classes, modules, and functions) should be open for extension, but closed for modification. In other words, it’s about designing classes that can be extended to add new functionality, without modifying the existing code.

The motivation behind the OCP is to make the software more flexible and maintainable. When code is closed for modification, it is much easier to add new functionality without having to worry about breaking existing code. Additionally, it makes it easier to maintain the codebase and fix bugs, since changes can be made in new code, rather than modifying existing code.

Let’s see an example of how to implement the OCP in Python:

Suppose we have a class Rectangle that represents a rectangle and its area. We also have another class AreaCalculator that calculates the total area of a list of rectangles.

class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height

def area(self):
return self.width * self.height

class AreaCalculator:
def __init__(self, shapes):
self.shapes = shapes

def total_area(self):
total = 0
for shape in self.shapes:
total += shape.area()
return total

Now, suppose we want to add the ability to calculate the area of a circle. We could add a method to the Rectangle class, but this would violate the OCP since we would have to modify the existing code.

A better approach is to create a new class Circle that represents a circle and its area:

class Circle:
def __init__(self, radius):
self.radius = radius

def area(self):
return 3.14 * self.radius * self.radius

Now, we can add a circle to the list of shapes and calculate its area, without having to modify the Rectangle or AreaCalculator classes:

shapes = [Rectangle(10, 20), Circle(5)]
calculator = AreaCalculator(shapes)
print(calculator.total_area())

In this example, the AreaCalculator class is open for extension, since we can add new shapes without modifying it. The Rectangle and Circle classes are closed for modification since we can add new functionality to them without affecting the AreaCalculator class.

The Open-Closed Principle is a crucial concept in software development that helps make code more flexible and maintainable. By following the OCP, we can write code that can be extended to add new functionality, without having to modify existing code. This leads to a more scalable and maintainable codebase, which is easier to debug, test, and extend.

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In other words, it is about designing class hierarchies that are flexible and allow objects of a subclass to be substituted for objects of a superclass.

The LSP helps ensure that the behavior of an object can be changed through inheritance, without affecting the behavior of other objects in the system. This allows for a more flexible and maintainable codebase since changes to a subclass do not affect the rest of the code.

Let’s see an example of how to implement the LSP in Python:

Suppose we have a class Rectangle that represents a rectangle:

class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height

def set_width(self, width):
self.width = width

def set_height(self, height):
self.height = height

def get_width(self):
return self.width

def get_height(self):
return self.height

def area(self):
return self.width * self.height

Now, suppose we want to add a class Square that represents a square:

class Square(Rectangle):
def set_width(self, width):
self.width = width
self.height = width

def set_height(self, height):
self.width = height
self.height = height

The Square class inherits from the Rectangle class, and overrides the set_width and set_height methods to ensure that a square always has equal width and height.

Now, suppose we have a function use_rectangle that uses a Rectangle object to calculate its area:

def use_rectangle(rectangle):
rectangle.set_width(4)
rectangle.set_height(5)
print(rectangle.area())

We can use the Square class as a substitute for the Rectangle class, since it inherits from Rectangle and implements the same interface:

square = Square(0, 0)
use_rectangle(square)

In this example, the Square class can be used as a substitute for the Rectangle class, without affecting the behavior of the use_rectangle function. This demonstrates the Liskov Substitution Principle, as objects of a subclass can be substituted for objects of a superclass without affecting the correctness of the program.

The Liskov Substitution Principle is a crucial concept in software development that helps ensure that class hierarchies are flexible and allow objects of a subclass to be substituted for objects of a superclass. By following the LSP, we can write code that is more flexible, maintainable, and scalable, since changes to a subclass do not affect the rest of the code.

Interface Segregation Principle (ISP)

The Interface Segregation Principle (ISP) states that clients should not be forced to depend on interfaces they do not use. In other words, it is about designing interfaces that are specific to the needs of each client, rather than creating a single, general-purpose interface that all clients must implement.

The ISP helps to prevent the creation of bloated, complex interfaces that are difficult to implement and maintain. By creating interfaces that are tailored to the needs of each client, we can create a more flexible and maintainable codebase.

Let’s see an example of how to implement the ISP in Python:

Suppose we have a class Rectangle that represents a rectangle:

class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height

def set_width(self, width):
self.width = width

def set_height(self, height):
self.height = height

def get_width(self):
return self.width

def get_height(self):
return self.height

def area(self):
return self.width * self.height

Now, suppose we have a class Circle that represents a circle:

class Circle:
def __init__(self, radius):
self.radius = radius

def set_radius(self, radius):
self.radius = radius

def get_radius(self):
return self.radius

def area(self):
return 3.14159 * self.radius ** 2

Finally, suppose we have a function calculate_area that calculates the area of a shape:

def calculate_area(shape):
return shape.area()

In this example, the Rectangle and Circle classes have different interfaces and the calculate_area function only requires the area method, which is present in both classes. This demonstrates the Interface Segregation Principle, as the calculate_area function is not forced to depend on interfaces that it does not use.

The Interface Segregation Principle is a crucial concept in software development that helps ensure that interfaces are tailored to the needs of each client, rather than creating a single, general-purpose interface that all clients must implement. By following the ISP, we can create a more flexible and maintainable codebase, since changes to one interface do not affect the behavior of other parts of the code.

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules, but both should depend on abstractions.

The DIP helps to ensure that code remains flexible and maintainable, even as it evolves over time. By decoupling high-level modules from low-level modules, we can make changes to low-level code without affecting the behavior of high-level code.

Let’s see an example of how to implement the DIP in Python:

Suppose we have a class Rectangle that represents a rectangle:

class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height

def set_width(self, width):
self.width = width

def set_height(self, height):
self.height = height

def get_width(self):
return self.width

def get_height(self):
return self.height

def area(self):
return self.width * self.height

Now, suppose we have a class Circle that represents a circle:

class Circle:
def __init__(self, radius):
self.radius = radius

def set_radius(self, radius):
self.radius = radius

def get_radius(self):
return self.radius

def area(self):
return 3.14159 * self.radius ** 2

Finally, suppose we have a class AreaCalculator that calculates the area of a list of shapes:

class AreaCalculator:
def __init__(self, shapes):
self.shapes = shapes

def total_area(self):
return sum([shape.area() for shape in self.shapes])

In this example, the AreaCalculator class depends on abstractions (the shape objects) rather than concrete implementations (the Rectangle and Circle classes). This demonstrates the Dependency Inversion Principle, as high-level modules (the AreaCalculator class) are decoupled from low-level modules (the Rectangle and Circle classes).

The Dependency Inversion Principle is a crucial concept in software development that helps ensure that code remains flexible and maintainable, even as it evolves over time. By following the DIP, we can create a more flexible and maintainable codebase, since changes to low-level code do not affect the behavior of high-level code.

If you like the post, don’t forget to clap. If you’d like to connect, you can find me on LinkedIn.

References:

Book “Agile Software Development, Principles, Patterns, and Practices”

--

--

Neeraj Kushwaha

https://www.learncsdesign.com “Walking on water and developing software from a specification are easy if both are frozen”