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++':
Code is clean if it can be understood and maintained easily by any team member.
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.
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.
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:
- 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.
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:
- S ingle Responsibility Principle
- O pen/Closed Principle
- L iskov Substitution Principle
- I nterface Segregation Principle
- D ependency 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.
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:
- A template engine to render the body of the email message.
- A translator for translating the message’s subject.
- 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
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
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
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
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
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.
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.