SOLID on cats

Every programmer has heard about the SOLID principles at least once. During interviews and exams at universities, many of us tried to remember what that very Liskov principle was about. However, it is unlikely that the goal of teachers and interviewers is to force us to memorize lines from textbooks. SOLID really helps you write quality code once you get the hang of it! If you haven't done this yet, welcome to cat. Let's take another look at how the well-known principles work. I promise – without stuffiness, we’ll look at everything using examples with cats.

Use the table of contents if you don't want to read the entire text:
The principle of sole responsibility
Open/closed principle
Barbara Liskov's Substitution Principle
Interface separation principle
Dependency Inversion Principle
Conclusion

The principle of sole responsibility

There should be separate areas for playing and eating.

The Single Responsibility Principle (SRP) states that each class or module should have only one reason to change. That is, it should perform only one task.

It sounds simple, but where can this principle be applied? And what does it even mean to “have only one reason for change”? Let's look at a cat's example.

My cat's name is Borya. Now I will try to implement it.

class BoryaCoolCat:
    def eat(self):
        print("Омномном")
    def play(self):
        print("Тыгыдык")
    def save_to_database(self):
        # Код для сохранения Борика в базу данных
        Pass

Boryan is an advanced guy – he can meow and keep himself in the database. Let's say the veterinarian told Borya to eat wet food only in the morning. Let's then split the eat function into breakfasts and other meals:

class BoryaCoolCat:
    def morning_eat(self):
        print("Омномном")
    def eat(self):
        print("Не люблю сухой корм")
    def play(self):
        print("Тыгыдык")
    def save_to_database(self):
        # Код для сохранения Борика в базу данных
        pass

Now let's say one of our big guy's toys will be the morning food from the table. To reflect this in code, let's split up the play function like we did with the eat function above:

class BoryaCoolCat:
    def morning_eat(self):
        print("Омномном")
    def eat(self):
        print("Не люблю сухой корм")
    def morning_eat_play(self):
        print("О конфетки, буду катать их по полу!")
    def play(self):
        print("Тыгыдык")
    def save_to_database(self):
        # Код для сохранения Борика в базу данных
        pass

It is already clear that the code is becoming not very clear. If you don’t know the history of its writing, it’s hard to guess how morning_eat differs from morning_eat_play. Now let’s make our cat a little more solid – we’ll combine methods with similar themes into classes. Those related to food (morning_eat and eat) will be placed in the CatFeeding class. Thus, this class will have only one reason for change – feeding. Similarly, we will move the morning_eat_play and play functions to the CatPlay class, and save_to_database to the CatDatabase.

class CatFeeding:
    def morning_eat(self):
        print("Омномном")
    def eat(self):
        print("Не люблю сухой корм")
class CatPlay:
    def morning_eat_play(self):
        print("О конфетки, буду катать их по полу!")
    def play(self):
        print("Тыгыдык")
class CatDatabase:
    def save_to_database(self, cat):
        # Код для сохранения объекта cat в базу данных
        print(f"{cat.name} сохранён в базу данных.")

Great, now each of the classes has only one reason for change – feeding, games or saving to the database! But the main thing here is to know when to stop and not create classes for each method.

Open/closed principle

If a cat learns to hiss, he should not forget how to meow.

Open/Closed Principle (OCP) – software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

Extensions, modifications, blah blah blah… Now, with Bori's help, we'll figure it all out! Let's give our boy a speech:

class BoryaCoolCat:
    def speak(self):
        return "Мяу!"

After living with him a little, I realized that in the morning he has two moods: an affectionate cutie and a cyborg killer. It will be strange if in both cases he chats the same way. Let's try to improve Boryan:

class BoryaCoolCat:
    def speak(self, nice_mood: bool):
        if nice_mood:
            return "Мур-мур-мур"
        return "Шшшшшшш"

We see that as our function gets larger, it becomes easier to make mistakes in it. If Bori gets in another mood, the function will grow. Let's add some solidity.

class BoryaCat:
    def speak(self):
        return "Мяу!"
class NiceCat(BoryaCat):
    def speak(self):
        return "Мур-мур-мур"
class AngryCat(BoryaCat):
    def speak(self):
        return "Шшшшшшш"

Look what our classrooms look like now. They are open to adding new logic, but closed to changing the current one. Changing the original logic is a very dangerous procedure. The method can be used in different places in the code, when changing it is very difficult to find all the errors. But if we do not change the original logic, but add new cases, the working code will not break, and we will be able to implement new functionality without problems!

Barbara Liskov's Substitution Principle

If we love toys, then we love mice and candy wrappers.

Barbara Liskov's Substitution Principle (LSP) states that objects of a subclass should be interchangeable with objects of a superclass without changing the desired properties of the program. This means that if class S is a subclass of class T, then objects of class T must be replaceable with objects of class S without violating program correctness.

Perhaps the most incomprehensible principle of all. When I read it for the first time, I felt like I was on the mat in my first year. What S? what T? Classes, subclasses… You couldn’t sit still, Liskov. But get ready, now we will defeat this principle once and for all!

Let's make a class of Borya's little things. In it we will create a method that will return how Borya rejoices and loves his toys.

class BoryasStuff:
    def enjoy(self):
        print("Ура")

Bori has a candy wrapper, my hands and everything that is on the table. All these are his things, so they should be inherited from BoryasStuff.

class CandyWrapper(BoryasStuff):
    def enjoy(self):
        print("Ура, шелестеть!")
class MommysHand(BoryasStuff):
    def enjoy(self):
        print("Ура, царапать!")
class TableThings(BoryasStuff):
    def enjoy(self):
        print("Урааа, скидывать на пол!")

I recently bought Bora an expensive mouse that can run away from him and make sounds. But, as usual, the more expensive the toy, the less Borya is interested in it. So its implementation will look like this:

class CoolMouse(BoryasStuff):
    def enjoy(self):
        raise NotImplementedError("Какая мышь? Где мой трижды погрызанный фантик?")

This is where Barbara Liskov’s principle is violated. The fact is that, according to it, we must be able to use all child classes in the same way as parent ones. But in the BoryasStuff class, unlike the child class CoolMouse, the enjoy method runs without problems.

If we can call BoryasStuff().enjoy(), then we should be able to call CoolMouse().enjoy(). To comply with the principle, we can either exclude the enjoy method from BoryasStuff, or not inherit CoolMouse from BoryasStuff, that's all!

class BoryasStuff:
    pass
class CandyWrapper(BoryasStuff):
    def enjoy(self):
        print("Ура, шелестеть!")
class MommysHand(BoryasStuff):
    def enjoy(self):
        print("Ура, царапать!")
class TableThings(BoryasStuff):
    def enjoy(self):
        print("Урааа, скидывать на пол!")
class CoolMouse(BoryasStuff):
    def hate(self):
        print("Какая мышь? Где мой трижды погрызанный фантик?")

Interface separation principle

There is no need for you to know how to swim if you will never do it.

The Interface Segregation Principle (ISP) states that clients should not depend on interfaces that they do not use.

Somehow we are messing around with class selections. Let's create a common cat interface. What can they do? Run, swim, and, of course, scream at 5 am.

class Cat:
    def run(self):
        pass
    def swim(self):
        pass
    def morning_yell(self):
        pass

But my Borik is a strong homemade guy. Once he was swimming in the bathtub and he really didn’t like it, so we decided not to torture him. But if we inherit Borya from Cat, theoretically someone could make him swim, which he clearly wouldn’t want. Let's separate the interfaces so we can only inherit what we need:

class Walkable:
    def run(self):
        pass
class Hateable:
    def morning_yell(self):
        pass
class Swimmable:
    def swim(self):
        pass

Everything is fine now! We can simply inherit Borya from Walkable and Hateable. This is the principle of interface separation. We shouldn't inherit from something we don't use.

Dependency Inversion Principle

The fact that cats meow does not affect all other animals. But the fact that animals make sounds affects cats.

The Dependency Inversion Principle states:

So many words and so hard to find meaning in them. Let's look at an example of how to use this principle. Most animals make some kind of sounds. Let's write an interface for this:

from abc import ABC, abstractmethod
class Sound(ABC):
    @abstractmethod
    def make_sound(self):
        pass

Now let's define the cat sound:

class CatSound(Sound):
    def make_sound(self):
        return "Мяу!"

See, cat sounds are inherited from animal sounds. Everything looks logical. Now let's implement Borya once again:

class BoryaCoolCat:
    def __init__(self, sound: Sound):
        self.sound = sound
    def speak(self):
        return self.sound.make_sound()

Did you notice the juice? In the class constructor we get a sound of type Sound. This means that if at some point we decide that Borya is not a cat, but, for example, a crocodile, we will not need to rewrite the BoryaCoolCat class. You just need to pass any other class that inherits from Sound into it!

This principle works very well in large projects. This is where the DI container comes to the rescue. Perhaps in future articles I will touch on this interesting topic.

Conclusion


At the end of our solid journey through principles and bowls, I would like to emphasize that all this is not just a set of rules. Yes, you can memorize them for an exam or job and not remember anymore. But they do help create better, more maintainable code.

Good code, like cats, requires attention and care. By applying SOLID principles, we not only improve its structure, but also make it more understandable for other developers (and for ourselves in the future). Ultimately, this allows us to focus on solving problems rather than dealing with the consequences of our crutches.

Creating quality code is not just a task, but an art. I hope this article has inspired you to look at the SOLID principles in a new way and begin to apply them in your practice.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *