How to Write Clean Code (in Python) With SOLID Principles | Principle #1

Without a toy example

A major misunderstanding is to confuse clean code with something that can be called “beautiful code.”

Professional programmers are not paid to write pretty code.

They are hired by development companies as experts to create customer value.

So what is clean code?

You might wonder what beautiful code looks like in the first place. Well, Your code can be pretty if it has few lines of code, or when it is in a good indented form instead of a long if-else chain, or when if there is a list comprehension instead of a long for loop.

But clean code is not just about writing short code or a well-formatted one. It is way different. Here is what clean code really is as Stephan Roth stated in his book 'Clean C++':

  1. Code is clean if it can be understood and maintained easily by any team member.

  2. Clean code is the basis of fast code. If your code is clean and test coverage is high, it only takes a few hours or a couple of days to implement, test, and deploy a change or a new function; not weeks or months.

  3. Clean code is the foundation for sustainable software; it keeps a software development project running over a long time without accumulating a large amount of technical debt. Developers must actively ensure that the software stays in shape for survival.

  4. Clean code is also the key to being a happier developer. It leads to a stress-free life. If your code is clean and you feel comfortable with it, you can keep calm in every situation, even when facing a tight project deadline.

All of the points mentioned here are true, but the key point is this:

  1. Clean code saves money! In essence, it’s about economic efficiency. Each year, development organizations lose a lot of money because their code is in bad shape.

Clean code ensures that the value added by the development organization remains high. Companies can earn money from its clean code for a long time.

Design principles

To make the code clean, you need to understand software principles. There are dozens expressed in Martin Robert's books but just five particular topics permeate the discussion of patterns and software design in general.

Here is a glimpse of these 5 principles:

The acronym SOLID was introduced by Michael Feathers to help one remember these principles easily. These principles are named as follows:

  • Single Responsibility Principle
  • Open/Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

These principles form the fundamental guidelines for building applications (not necessarily object-oriented). Following these principles while writing your code will help you to build a robust, extensible, and maintainable code base.

Moreover, these principles also form a vocabulary with which to convey the underlying ideas between other team members or as a part of technical documentation.

1. Single Responsibility Principle

In this tutorial, we will talk about the first design principle: Single Responsibility Principle (SRP).

Will illustrate the concepts with a real-world example implemented in Python and shown in a UML diagram so that you can connect the dots between the classes we will design and have a visual understanding of what's going on.

Definition:

A class should have one, and only one, reason to change. ~ Robert Martin

Now, a "reason to change" means responsibility. So each responsibility is a reason to change the code.

Example: Email Confirmation Notification

Let's take a look at an example we probably all recognize. When someone subscribes to a mailing list, there is a confirmation email sent to that new user.

There are three dependencies for that email service:

  1. A template engine to render the body of the email message.
  2. A translator for translating the message's subject.
  3. A mailer for sending the confirmation email message.

These are all injected by their interface (which is good) like in the UML diagram below:

UML diagram of the initial situation. (Designed with plantuml)

Let's first see what the ConfirmationMailMailer class looks like:

class ConfirmationMailMailer:
    def __init__(
        self,
        template_engine: TemplateEngineInterface,
        translator: TranslatorInterface,
        mailer: MailerInterface
    ):
        self._template_engine = template_engine
        self._translator = translator
        self._mailer = mailer
        
    def sent_to(self, user):
        message = self._create_message_for(user)
        self.send_message(message)

    def _create_message_for(self, user):
        subject = self._translator.translate("Confirm your email address")
        body = self._template_engine.render("confirmation_email.html.tpl", user.get_confirmation_code())
        message = Message(subject, body)
        message.set_to(user.email)
        return message

    def send_message(self, message):
        self._mailer.send(message)

Responsibilities are reasons to change

So that class has now two jobs or two responsibilities; to create a confirmation email message and send it to the user. These two responsibilities are also its two reasons to change. Whenever the requirements change regarding creating the messages or regarding sending them, this class will have to be modified.

Does that class has to be modified when just one of the two responsibilities requires a change? Sadly, yes. And this is a problem because most of the logic in the class may have nothing to do with the requested change itself!

So what we should do here is to try to minimize the number of responsibilities of each class to do just one job. Hence the Single Responsibility principle. This would at the same time minimize the chance that the class has to be opened for modification.

How to spot SRP violations

To recognize if there is a violation of the single responsibility principle, there is a list of symptoms that can be used to detect such violations:

  • The class has many instance variables.
  • The class has many public methods.
  • Each method of the class uses different instance variables.
  • Specific tasks are delegated to private methods.

These are all good reasons to extract the so-called "collaborator classes" from the class and separate them into smaller classes to adhere to the Single Responsibility Principle.

How to refactor using collaborator classes

We now know that we should refactor the ConfirmationMailMailer class by extracting collaborator classes.

Since this class is a "mailer," we let it keep the responsibility of sending the message to the user. But we extract the responsibility of creating the message such as the _create_message_for method which contains the object instantiation of the Message class.

Creating a message is a bit more complicated than a simple object instantiation. It even requires several dependencies. This calls for a dedicated factory class; the ConfirmationMailFactory class.

Introducing the ConfirmationMailFactory. (Designed with plantuml)

Now, let's see how both the ConfirmationMailMailer and the ConfirmationMailFactory are implemented:

class ConfirmationMailMailer:
    def __init__(
        self,
        confirmation_mailer_factory: ConfirmationMailerFactory,
        mailer: MailerInterface
    ):
        self._mailer = mailer
        self._confirmation_mailer_factory = confirmation_mailer_factory
        
    def sent_to(self, user):
        message = self._create_message_for(user)
        self.send_message(message)

    def _create_message_for(self, user):
        return self._confirmation_mailer_factory.create_message_for(user)

    def send_message(self, message):
        self._mailer.send(message)

class ConfirmationMailFactory:
    def __init__(
        self,
        template_engine: TemplateEngineInterface,
        translator: TranslatorInterface
    ):
        self._template_engine = template_engine
        self._translator = translator

    def create_message_for(self, user):
        # Create an instance of Message based on the given User
        message = ...
        return message
    )

Now the message creation logic of the confirmation mail has been moved to ConfirmationMailFactory. It would be even better if an interface was defined for the factory class, but it’s fine for now.

Advantages of having a Single Responsibility

When we refactored to single responsibilities, both of the classes are now easier to test. You can now test both responsibilities separately.

Now, you can verify the correctness of the created message by testing _create_message_for() method of ConfirmationMailFactory. You can also test the send_to() method of ConfirmationMailMailer separately.

This makes you focus on testing that the message is sent by mocking up the complete message-creation process.

In general, you will notice that classes with single responsibilities are easier to test because the class is now smaller thus there are fewer tests to cover the test cases.

Final thoughts

Finally, smaller classes are also simpler to maintain and easier for your mind to grasp, and not to get lost in the code and all implementation details where the classes belong.

Classes now have just one responsibility; one reason to change.

Follow up with the related articles to this series:

Published on medium

Buy me a cup of coffee

Join the conversation

Download the ebook

Download the eBook to write cleaner Python code

Get the ebook