SOLID Principles tutorial for python lovers
Tags: python, beginners, design, codenewbie
Published: 2023-03-05 09:16:50

SOLID principles are five essential rules for designing a great class structure in Object-Oriented programming. 
They are a set of guidelines to follow for creating well-organized and maintainable code.
In this article, you'll learn everything you need to know to use SOLID principles illustrated using python.

We will start with a basic design in python that breaks those principles and we’ll improve it step by step.

Let's get started!

---

_This tutorial is available in video format [here](https://youtu.be/36Ia-JdW_RI)._

----


SOLID stands for:
- Single Responsibility
- Opened to extend and closed to change
- Liskov substitution
- Interface segregation
- Dependency inversion

Below we have a simple code that calls chatGPT API to generate a birthday message for someone and send that message to the person on his WhatsApp phone number.

```python
class AIBirthdayMessageSender:
    """Handle the feature of sending a birthday message to someone."""

    def __init__(self):
        self.message = ''

    def generate_message(self, receiver_name: str):
        print("Authenticate to ChatGPT API and request for birthday message")
        self.message = f"Happy birthday {receiver_name} from ChatGPT"

    def send(self, phone: str):
        print(f"Authenticate to Whatsapp API and send message to {phone}")
        print(f"message sent: ~~~{self.message}~~~")

if __name__ == "__main__":
    birthday_msg_sender = AIBirthdayMessageSender()
    birthday_msg_sender.generate_message("Johnathan Smith")
    birthday_msg_sender.send(phone="022990149224")
```

For simplicity, we've just printed what the code is supposed to do instead of the real code.

----

## 1. Single Responsibility
This principle states that a class should have one and only one task.
If you run the code you'll see this output:
```bash
Authenticate to ChatGPT API and request for birthday message
Authenticate to Whatsapp API and send message to 022990149224
message sent: ~~~Happy Birthday Johnathan Smith from ChatGPT~~~
```
So it looks well.

But, there is an issue. The class `AIBirthdayMessageSender`, as its names suggest should only be responsible
for sending the message. But as we can see it also handle the message generation from chatGPT. So it does two unrelated things which break the **Single responsibility** principle.

To fix that we can move the functionality of AI message generation into another class:

```python
class ChatGptBirthdayMsgGenerator:
    """Generate a birthday message from chat GPT."""

    def __init__(self, receiver_name: str):
        self.receiver_name = receiver_name

    def generate(self):
        print("Authenticate to ChatGPT API and request for birthday message")
        return f"Happy Birthday {self.receiver_name} from ChatGPT"


class AIBirthdayMessageSender:
    """Handle the feature of sending a birthday message to someone."""

    def __init__(self, message: str):
        self.message = message

    def send(self, phone: str):
        print(f"Authenticate to Whatsapp API and send message to {phone}")
        print(f"message sent: ~~~{self.message}~~~")


if __name__ == "__main__":
    msg_generator = ChatGptBirthdayMsgGenerator(receiver_name="Johnathan Smith")
    message = msg_generator.generate()
    birthday_msg_sender = AIBirthdayMessageSender(message)
    birthday_msg_sender.send(phone="022990149224")
```

And if you run, it should still work.

----

## 2. Opened to extend and closed to change
This principle states that we don't need to modify existing code before being able to repeate a behavior.

Let's say in the future we want to be able to send the message via email, or to a Facebook account instead of to a WhatsApp number.
We will be obliged to modify `AIBirthdayMessageSender` like this probably:
```python
class AIBirthdayMessageSender:

    def send_phone(self, phone: str):
        pass

    def send_email(self, email: str):
        pass

    def send_facebook(self, username: str):
        pass
```

But by doing that we break the **Opened/close** principle because we should have written our class from in the  way
that we don't need to modify the existing class if we want to add a new feature, but we just need to extend them.

We can fix it by using inheritance:

```python
from abc import ABC, abstractmethod

class ChatGptBirthdayMsgGenerator:
    """Generate a birthday message from chat GPT."""

    def __init__(self, receiver_name: str):
        self.receiver_name = receiver_name

    def generate(self):
        print("Authenticate to ChatGPT API and request for birthday message")
        return f"Happy Birthday {self.receiver_name} from ChatGPT"

class AIBirthdayMessageSender(ABC):

    def __init__(self, message: str):
        self.message = message

    @abstractmethod
    def send(self, phone: str):
        pass

class WhatsAppBirthdayMessageSender(AIBirthdayMessageSender):
    """Handle the feature of sending a birthday message to someone."""

    def send(self, phone: str):
        print(f"Authenticate to Whatsapp API and send message to {phone}")
        print(f"message sent: ~~~{self.message}~~~")

class EmailBirthdayMessageSender(AIBirthdayMessageSender):
    """Handle the feature of sending a birthday message to someone."""

    def send(self, email: str):
        print(f"Authenticate to Email API and send mail to {email}")
        print(f"message sent: ~~~{self.message}~~~")

if __name__ == "__main__":
    msg_generator = ChatGptBirthdayMsgGenerator(receiver_name="Johnathan Smith")
    message = msg_generator.generate()

    whatsapp_sender = WhatsAppBirthdayMessageSender(message)
    whatsapp_sender.send(phone="022990149224")

    email_sender = EmailBirthdayMessageSender(message)
    email_sender.send(email="j_smith@gmail.com")
```
If you run the code again, you should see:
```
Authenticate to ChatGPT API and request for birthday message
Authenticate to Whatsapp API and send message to 022990149224
message sent: ~~~Happy Birthday Johnathan Smith from ChatGPT~~~
Authenticate to Email API and send mail to j_smith@gmail.com
message sent: ~~~Happy Birthday Johnathan Smith from ChatGPT~~~
```
It still works.

----

## 3. Liskov substitution
This principle states that we should be able to replace an object of a given class with any object of its subclasses without breaking or changing the correctness of the program.

In our code we have `WhatsAppBirthdayMessageSender` and `EmailBirthdayMessageSender` which are subclasses of  `AIBirthdayMessageSender`.
But `AIBirthdayMessageSender.send()` method defines a parameter `phone` that cannot be applied to all subclasses, especially to `EmailBirthdayMessageSender` which doesn't have `phone` but `email` instead. This breaks the `Liskov substitution` principle and to fix that, we can just remove the argument `phone` from the parent, because it's not an argument that is general to `AIBirthdayMessageSender`, like `message` in the constructor, but it's specific to the `WhatsAppBirthdayMessageSender`.

Here is it new code:
```python
class AIBirthdayMessageSender(ABC):

    def __init__(self, message: str):
        self.message = message

    @abstractmethod
    def send(self):
        pass
```
That way, we will no longer be allowed to call `AIBirthdayMessageSender.send()` with `phone` as it belongs to `WhatsAppBirthdayMessageSender`
subclass.

----

## 4. Interface segregation
This principle states that a class or a method should not be forced to do what it's not supposed to.
In other words, it's better to have several different interfaces that do different things than a single interface that tries to be accommodated to do all the things.

Another way we could have modified our code to respect the **Liskov substitution** principle is to do this:
```python
class AIBirthdayMessageSender(ABC):
    ...
    @abstractmethod
    def send(self, phone=None, email=None, facebook_username=None,):
        pass
```
We have a single global interface in the parent class, with all the possible arguments for the `send` method of all it subclasses, and we make them optional.
so for each subclass, we can just pass the needed arguments and the others will be `None`.

That will work. But it breaks the **Interface segregation** principle because we are forcing the poor 
 `AIBirthdayMessageSender.send()` method that didn't ask for anything, to handle all subclasses cases.
So the right way is still what we have done previously. Each `send` method of each subclass defined its specific own arguments.

----


## 5. Dependency Inversion
This last principle states that high-level classes should depend on abstractions and not on concrete classes or low-level classes.

First, let's try to add a high-level class. It's just the main class that calls other classes to handle the business logic.

We have added a `User` class and a business logic class called `AIBirthdayManager` that reuse low-level classes like `ChatGptBirthdayMsgGenerator`
or `WhatsAppBirthdayMessageSender` to handle some features around the birthday of a given `User`:

We have modified `ChatGptBirthdayMsgGenerator` to inherit it from an abstraction:
```python
class User:
    def __init__(self, name, email, phone):
        self.name = name
        self.email = email
        self.phone = phone

class AIBirthdayManager:

    def __init__(self, user: User):
        self.user = user

    def order_a_gift(self):
        pass

    def send_happy_birthday_message(self):
        msg_generator = ChatGptBirthdayMsgGenerator(self.user.name)
        message = msg_generator.generate()
        whatsapp_sender = WhatsAppBirthdayMessageSender(message)
        whatsapp_sender.send(phone="022990149224")

if __name__ == "__main__":
    user = User("Johnathan Smith", "j_smith@gmail.com", "022990149224")
    birthday_manager = AIBirthdayManager(user=user)
    birthday_manager.send_happy_birthday_message()
```
The issue with this is that, in the future if we want to change the AI provider to a different one, like BardAI for instance, we will be obliged to modify the `AIBirthdayManager` class which breaks **Opene/close** principle.
That's why `AIBirthdayManager` should not directly depend on `ChatGptBirthdayMsgGenerator`, but on an abstraction of it like this:
```python
class AIBirthdayMsgGenerator(ABC):
    def __init__(self, receiver_name: str):
        self.receiver_name = receiver_name

    @abstractmethod
    def generate(self):
        pass

class ChatGptBirthdayMsgGenerator(AIBirthdayMsgGenerator):
    """Generate a birthday message from chat GPT."""
    def generate(self):
        print("Authenticate to ChatGPT API and request for birthday message")
        return f"Happy Birthday {self.receiver_name} from ChatGPT"

...

class AIBirthdayManager:
    def __init__(self, user, ai_class: AIBirthdayMsgGenerator):
        self.user = user
        self.ai_class = ai_class

    def send_happy_birthday_message(self):
        msg_generator = self.ai_class(self.user.name)
        ...

if __name__ == "__main__":
    user = User("Johnathan Smith", "j_smith@gmail.com", "022990149224")
    birthday_manager = AIBirthdayManager(
        user=user,
        ai_class=ChatGptBirthdayMsgGenerator
    )
    birthday_manager.send_happy_birthday_message()
```
Now our high-level module `AIBirthdayManager` depends on the abstraction `AIBirthdayMsgGenerator` and to change the AI provider we just have to create another subclass of `AIBirthdayMsgGenerator` and pass it to `ai_class` argument in the constructor.
Let's implement BardAI:
```python
class BardAIBirthdayMsgGenerator(AIBirthdayMsgGenerator):
    """Generate a birthday message from BardAI."""
    def generate(self):
        print("Authenticate to BardAI API and request for birthday message")
        return f"Happy Birthday {self.receiver_name} from BardAI"

if __name__ == "__main__":
    user = User("Johnathan Smith", "j_smith@gmail.com", "022990149224")
    birthday_manager = AIBirthdayManager(
        user=user,
        ai_class=BardAIBirthdayMsgGenerator
    )
    birthday_manager.send_happy_birthday_message()
```
The new output should be:
```
Authenticate to BardAI API and request for birthday message
Authenticate to Whatsapp API and send message to 022990149224
message sent: ~~~Happy Birthday Johnathan Smith from BardAI~~~
```

That's all.
Thanks for reading.
Don't hesitate to check the video version of this article [here](https://youtu.be/36Ia-JdW_RI).