Regardless of your knowledge level, as a programmer you love to write awesome code. It’s what we do. We like it and we do it every single day. But, we all know that writing awesome code is not easy at all. So, how can we improve the code we produce every day?
An awareness (or a reminder!) of SOLID principles is beneficial here. SOLID is a group of five principles that when applied correctly can help us produce better code.
So, what are the SOLID principles?
SOLID is a mnemonic acronym coined by Uncle Bob back in the early 2000s. It represents a group of five principles:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Sounds good? Great. Let’s take a look at each of these principles.
Single Respnosibility Principle
In my opinion, this is the easiest principle to understand. What SRP says is:
Every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class.
What does this mean? Basically, every class in your app must have a single responsibility. Easy as that. The best way to detect if your class obeys this principle is to answer this question:
What does this class do?
If your answer contains the word AND, then your class does not obey the SRP.
Lets see a quick example. There’s the Student class and every student has grades for different terms.
Some of you may already be thinking “that is wrong sir!”, some of you may not. Regardless of that, yes, this does not obey the SRP. The reason is that the class Student contains the logic that calculates the average grade for each term. The responsibility of
Student is to hold info/logic about the student, not the grades. The logic that calculates the grades should be part of a class
Let’s refactor the code.
You can see that now
Grade holds the logic for calculation of the grade and Student only stores the grades into a collection. Now this complies to the SRP, because, every class has it’s own responsibility.
The Open/closed principle (OCP) is a principle whose definition is:
One software entity (class/module) must be open for extension but closed for modification.
What does this mean? Once a class implements the current scope of requirements, the implementation should not need to change in order to fulfil future requirements.
It doesn’t make sense? Let’s take a look at a quick example.
Simple logger class right? It has a format string and sends the current time and the message to STDOUT. Cool, simple enough. Lets test it:
Awesome. But, what would happen if someone in the future needs the logger to prepend the string “[LOG]” to the log message, so the output would look like:
For example, a programmer that does not know about the OCP can possibly do this change:
And the output of the new MyLogger class would be:
Everything looks good, right? But, wait a second? Does it?
Think about this - if this was a core class of an app, the change we introduced to the
format_string would break the functionality of that classes that rely on the
MyLogger class. There’s the possibility that a whole world out there relies on the former funcionality of the class, but now, that we changed it, a lot of things can break. This is a violation of the OCP and it is bad!
So, what is the good way to do it? Inheritance! Or object composition!
Let’s see an example that uses inheritance:
Nice, works as expected! What about the functionality of
Great! So, what did we just do? We extended the
MyLogger class and created a brand new class called
NewCoolLogger that extends the former class. Now the code that relies on the functionality of the old logger will not break due to the changes we introduced. The old logger will work just like it did before and the new one will provide the new functionality that the programmer wanted.
Also, I mentioned object composition. Take this refactor in cosideration:
You can notice that the log method receives an optional parameter called
formatter. The format of the log string is responsibility of the
MyLogFormatter class not the logger class itself. This is good because now
MyLogger#log can accept different formatter classes that will set the format of the log message. For example, you can create
ErrorLogFormatter that will prepend
[ERROR] to the log message but
MyLogger will not care because all it needs is a string that it will send to STDOUT.
Liskov substitution principle
Barbara Liskov defined the principle within these lines:
If S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).
Honestly, I found this definition pretty hard to understand. So, after some thinking, this is what it boils down to:
There is a class
Bird. And there are two objects,
obj2. The class of
Duck which is a child-class of
Bird. Let’s say we discover that
obj2’s class is
Pigeon, which is also a child-class of
Bird. Liskov substitution principle states that in this situation, when
obj2 has a type of
Bird sub-class and
obj1 which is of class
Duck which is also a sub-type of
Bird, I should be able to treat
obj2 in the same way - as
Still confusing? Take a look at the example below.
Hope that explained LSP.
Interface segregation principle
The interface-segregation principle (ISP) states that: > No client should be forced to depend on methods it does not use.
Simple as that. Lets see some code examples and explain them.
In this example, there are
Technician classes. Both,
Technician use the
Computer in a different way. The programmer uses the computer for typing, but the technician knows how to change the computer hard drive. What Interface Segregation Principle (ISP) enforces is that one class should not depend on methods it does not use.
In our case,
Programmer is unnecessarily coupled to the
Computer#change_hard_drive method because it does not use it, but the state changes that this method enforces can affect the
Programmer. Let’s refactor the code to obey the LSP.
After this refactor the
Technician uses a different object from the type
ComputerInternals which is isolated from the state of the
Computer. The state of the
Computer object can be influenced by the
Programmer but the changes wont affect the
Technician in any way.
Dependency inversion principle
Dependency inversion principle refers to a specific form of decoupling software modules. It’s definition has two parts:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend upon details. Details should depend upon abstractions.
I know that this might be a bit confusing. But, before we jump to a example I want to make sure that you must not mix Dependecy Inversion Principle with Dependency Injection. The later is a technique (or pattern) and the former is the principle.
Having that said, lets see the example:
Report class is used to generate an XML report. In it’s initializer we setup the report and its body. The
XmlFormatter class to convert the body of the report to XML. Easy as that.
Let’s think a bit about this class. Look at it’s name -
Report. It’s a generic name and it tells us that it will return a report of some kind, but, it doesnt say much about it’s format. In fact, in our example, we can easily rename our class to
XmlReport since we know the implementation details. But insead of making it very specific, let’s think about abstracting this code.
Right now, our class is dependant on the
XmlFormatter class and it’s interface i.e.
Report right now is dependent on a detail, not on abstraction. It knows that there must be a class
XmlFormatter so it can work. Also, another question - what would happen if we wanted an CSV report? Or a JSON report? We’d have to have multiple methods like
print_json. This means that our class right now is very tied to the details, it knows about the formatter class type instead of knowing just how to use it (abstraction).
Let’s refactor it:
Look at the
generate. How is this better? Well, if we wanted CSV reports, all we would need is to add the following class:
Report#print method would accept a
CSVFormatter object as a parameter which would convert the report body into a CSV string.
That pretty much sums up all of the five SOLID principles. All of our refactoring examples are very basic and we just scratched the surface. I’m sure that in your carreer as a programmer you’d come on to much more complex problems. But, be assured that having SOLID foundations can definitely help you to write better code that is easier to maintain.