How can applying the SOLID principles make the code better?
Tags: solidprinciples, architecture, python, beginners
Published: 2024-03-19 16:14:21

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
```python
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def save(self):
        pass

    def send_email(self):
        pass
```
SRP
```python
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.
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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_

Next part: [How to put SOLID principles into practice ](https://dev.to/antonov_mike/how-to-put-solid-principles-into-practice-3jn3)