Applying the SOLID principles to your code can significantly enhance its quality, making it more understandable, flexible, maintainable, and scalable. Here’s how each principle contributes to improving code quality:
1. Single Responsibility Principle (SRP)
The Single Responsibility Principle states that a class should have only one reason to change. This principle aims to separate behaviours so that if bugs arise as a result of your change, it won’t affect other unrelated behaviours.
If each class has only one reason to change, you make the code easier to maintain. Changes are more localized, reducing the risk of introducing bugs. And since the responsibilities of each class are more clearly defined, such code is easier to read.
No SRP
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save(self):
pass
def send_email(self):
pass
SRP
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
def save(self, user):
pass
class EmailService:
def send_email(self, user):
pass
To address business responsibilities, we might consider what business logic or rules apply to the User and how they can be encapsulated within a class.
2. Open/Closed Principle (OCP)
Classes should be open for extension, but closed for modification. This means that you should be able to add new functionality without changing the existing code.
OCP allows new functionality to be added without modifying existing code, making the system more adaptable to change. And you minimize the risk of introducing bugs when extending the functionality of your system.
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def process_payment(self, amount):
pass
class CreditCardPayment(Payment):
def process_payment(self, amount):
print(f"Processing credit card payment of {amount}")
class OnlinePayment(Payment):
def process_payment(self, amount):
print(f"Processing online payment of {amount}")
class CryptoCurrencyPayment(Payment):
def process_payment(self, amount):
print(f"Processing crypto payment of {amount}")
# New class that uses a specific payment method passed as an argument
class PaymentProcessing:
def concrete_payments(self, payment_method: Payment, amount):
payment_method.process_payment(amount)
credit_card_payment = CreditCardPayment()
some_object = PaymentProcessing()
some_object.concrete_payments(credit_card_payment, 100)
online_payment = OnlinePayment()
some_object.concrete_payments(online_payment, 200)
crypto_payment = CryptoCurrencyPayment()
some_object.concrete_payments(crypto_payment, 300)
3. Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that if a program is using a base class, it should be able to use any of its subclasses without the program knowing it. In other words, the subclasses should be substitutable for their base class.
LSP ensures that subclasses can be used in place of their base classes without altering the correctness of the program, making the code more robust. In addition, you can reuse code because code that works with the base class will also work with any of its subclasses.
No LSP
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def set_side(self, side):
self.width = side
self.height = side
LSP
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * (self.radius ** 2)
4. Interface Segregation Principle (ISP)
The Interface Segregation Principle states that no client should be forced to depend on interfaces they do not use. This means that a class should not have to implement methods it doesn’t use. Clients should not be forced to depend on methods that they do not use.
By ensuring that interfaces are small and focused, so classes are not forced to depend on interfaces they do not use. This makes the system more flexible and adaptable to change.
No ISP
class Worker:
def work(self):
pass
def eat(self):
pass
class Robot(Worker):
def work(self):
pass
def eat(self):
raise NotImplementedError("Robots don't eat.")
ISP
class Worker:
def work(self):
pass
class Eater:
def eat(self):
pass
class Human(Worker, Eater):
def work(self):
pass
def eat(self):
pass
class Robot(Worker):
def work(self):
pass
5. Dependency Inversion Principle (DIP)
High-level modules depend on abstractions rather than on low-level modules. This makes the code more flexible and easier to understand. It becomes easier to write unit tests for individual components. Each component can be tested in isolation, without needing to consider the behavior of other components.
No DIP
class LightBulb:
def __init__(self):
self.is_on_state = False
def turn_on(self):
self.is_on_state = True
print("Light is on")
def turn_off(self):
self.is_on_state = False
print("Light is off")
def is_on(self):
return self.is_on_state
class ElectricPowerSwitch:
def __init__(self):
self.light_bulb = LightBulb()
def press(self):
if self.light_bulb.is_on():
self.light_bulb.turn_off()
else:
self.light_bulb.turn_on()
switch = ElectricPowerSwitch()
switch.press()
switch.press()
DIP
from abc import ABC, abstractmethod
class Switchable(ABC):
@abstractmethod
def turn_on(self):
pass
@abstractmethod
def turn_off(self):
pass
@abstractmethod
def is_on(self):
pass
class LightBulb(Switchable):
def __init__(self):
self.is_on_state = False
def turn_on(self):
self.is_on_state = True
print("Light is on")
def turn_off(self):
self.is_on_state = False
print("Light is off")
def is_on(self):
return self.is_on_state
class ElectricPowerSwitch:
def __init__(self, switchable: Switchable):
self.switchable = switchable
def press(self):
if self.switchable.is_on():
self.switchable.turn_off()
else:
self.switchable.turn_on()
light_bulb = LightBulb()
switch = ElectricPowerSwitch(light_bulb)
switch.press()
switch.press()
Conclusion
By adhering to the SOLID principles, developers aim to create a system that is easy to understand, easy to change, and easy to maintain. These principles guide the design of software in a way that is robust, flexible, and adaptable to future changes. They help in creating a system that is not only functional but also scalable and maintainable, making it a better choice for long-term projects.
Image created by Bing