DEV Community

Cover image for How can applying the SOLID principles make the code better?
Antonov Mike
Antonov Mike

Posted on • Edited on

How can applying the SOLID principles make the code better?

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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.") 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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() 
Enter fullscreen mode Exit fullscreen mode

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() 
Enter fullscreen mode Exit fullscreen mode

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

Next part: How to put SOLID principles into practice

Top comments (4)

Collapse
 
antonov_mike profile image
Antonov Mike

An example of a simple architecture based on SOLID principles. This is still not a working application because it has no business logic.
permanent link
repository (it may change later)

Collapse
 
riverajefer profile image
Jefferson Rivera

Nice.
Excellent examples.

Collapse
 
antonov_mike profile image
Antonov Mike

Thank you very much!

Collapse
 
antonov_mike profile image
Antonov Mike

Grab code examples here