SOLID Design Principles Examples in Python
I already explained what is SOLID Principles in a previous article and here are examples in Python for each of the SOLID principles to make it more clear:
Single Responsibility Principle (SRP)
The SRP states that a class should have only one reason to change. In other words, a class should have only one responsibility. Here's an example of a class that violates the SRP:
class Employee:
def calculate_pay(self):
# Calculate the employee's pay
pass
def save_to_database(self):
# Save the employee's data to the database
pass
In this example, the Employee
class has two responsibilities: calculating the employee's pay and saving the employee's data to the database. To follow the SRP, we could split this class into two separate classes:
class Employee:
def calculate_pay(self):
# Calculate the employee's pay
pass
class EmployeeRepository:
def save_to_database(self, employee):
# Save the employee's data to the database
pass
Now we have one class responsible for calculating the employee's pay and another class responsible for saving the employee's data to the database.
Open/Closed Principle (OCP)
The OCP states that classes should be open for extension but closed for modification. In other words, you should be able to add new functionality to a class without changing its existing code. Here's an example of a class that violates the OCP:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def calculate_bonus(self):
return self.salary * 0.1
In this example, the Employee
class calculates a bonus based on the employee's salary. If we want to calculate a bonus based on other factors, such as the employee's performance or the company's revenue, we would need to modify the Employee
class. To follow the OCP, we could use inheritance to create a new class for calculating bonuses:
class BonusCalculator:
def calculate_bonus(self, employee):
pass
class SalaryBonusCalculator(BonusCalculator):
def calculate_bonus(self, employee):
return employee.salary * 0.1
class PerformanceBonusCalculator(BonusCalculator):
def calculate_bonus(self, employee):
# Calculate bonus based on performance
pass
class RevenueBonusCalculator(BonusCalculator):
def calculate_bonus(self, employee):
# Calculate bonus based on revenue
pass
Now we have one class responsible for calculating bonuses and multiple subclasses that can calculate bonuses based on different criteria.
Liskov Substitution Principle (LSP)
The LSP states that objects of a superclass should be replaceable with objects of a subclass without changing the correctness of the program. In other words, subclasses should be able to be used in place of their parent classes without causing unexpected behavior. Here's an example of a class hierarchy that violates the LSP:
class Bird:
def fly(self):
pass
class Ostrich(Bird):
def fly(self):
raise NotImplementedError()
In this example, the Ostrich
class is a subclass of Bird
, but it cannot fly. If we try to use an Ostrich
object in place of a Bird
object, we will get an unexpected NotImplementedError
. To follow the LSP, we could split the Bird
class into two separate classes:
class Bird:
def fly(self):
pass
class FlyingBird(Bird):
def fly(self):
pass
class Ostrich(Bird):
pass
Now we have one class for birds that can fly and another class for birds that cannot fly.
Interface Segregation Principle (ISP)
The ISP states that clients should not be forced to depend on methods they do not use. In other words, interfaces should be tailored to the needs of their clients. Here's an example of a class that violates the ISP:
class Printer:
def print(self, document):
pass
def scan(self, document):
pass
In this example, the Printer
class provides both printing and scanning functionality. If a client only needs printing functionality, they will be forced to depend on the scan
method. To follow the ISP, we could split the Printer
class into two separate interfaces:
class Printer:
def print(self, document):
pass
class Scanner:
def scan(self, document):
pass
Now we have two separate interfaces, one for printing and one for scanning, that clients can use independently.
Dependency Inversion Principle (DIP)
The DIP states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. In addition, abstractions should not depend on details. Instead, details should depend on abstractions. Here's an example of a class that violates the DIP:
class EmployeeService:
def __init__(self, employee_repository):
self.employee_repository = employee_repository
def get_employee_by_id(self, employee_id):
return self.employee_repository.get_by_id(employee_id)
In this example, the EmployeeService
class depends on the EmployeeRepository
class, which is a low-level module. To follow the DIP, we could introduce an abstraction that both the EmployeeService
and EmployeeRepository
classes depend on:
class EmployeeRepository:
def get_by_id(self, employee_id):
pass
class EmployeeService:
def __init__(self, employee_repository):
self.employee_repository = employee_repository
def get_employee_by_id(self, employee_id):
return self.employee_repository.get_by_id(employee_id)
class SqlEmployeeRepository(EmployeeRepository):
def get_by_id(self, employee_id):
# Retrieve employee from SQL database
pass
class MongoEmployeeRepository(EmployeeRepository):
def get_by_id(self, employee_id):
# Retrieve employee from MongoDB
pass
Now we have an abstraction, the EmployeeRepository
class, that both the EmployeeService
and SqlEmployeeRepository
and MongoEmployeeRepository
classes depend on. This allows us to easily switch between different database implementations without changing the EmployeeService
class.
These examples show how each of the SOLID principles can be applied in Python code to create more maintainable, flexible, and reusable software.
Conclusion
The SOLID principles provide a set of guidelines for writing clean and maintainable code. By following these principles, we can write code that is more modular, flexible, and reusable, making it easier to maintain and extend over time.
In Python, we can apply the SOLID principles in a variety of ways, such as using inheritance and polymorphism to adhere to the LSP, creating small and focused classes that adhere to the SRP, using dependency injection to adhere to the DIP, and splitting interfaces to adhere to the ISP. By applying these principles, we can write code that is easier to understand, modify, and extend, ultimately leading to more robust and maintainable software.
It's worth noting that while the SOLID principles are generally considered to be good coding practices, they are not always applicable in every situation. As with any programming guideline or best practice, it's important to use good judgement and apply the principles in a way that makes sense for the specific requirements and constraints of your project.
In conclusion, the SOLID principles are an important set of guidelines for writing clean and maintainable code. By following these principles in our Python code, we can create software that is more flexible, reusable, and easier to maintain and extend over time.