In this post we will discuss how we can leverage SOLID principles to write “Clean Code”.
What bad code does to us
- Hard to debug
- Hard to understand
- Difficult to extend or modify
- Modifying one feature breaks others
- Hard to test
We definitely don’t want to be in any of these situations. Let us get familiar with some concepts that will help us avoid these.
Single Responsibility Principle (SRP)
A class or function should have one and only one reason to change
What do we think about the above class. Let’s assume that it’s part of a blogging system. What is the problem with this class. “Content management” will want to update if there is a change in content, “Display management” will want to update if we change how we print the blog and finally “Persistence management” will want to update if we choose to persist the blog some other way. Too many people having stake in a single class is never a good thing. What is the responsibility of this class? Maintain blog information, print a blog and persist a blog. As we can see, it clearly has more than one responsibility.
It might be tricky to identify the single responsibility, as you see all the functions are related to a blog. The trick is to think about different entities that might be interested to change this class. So if there are more than 1 party interested in changing the class or function we are clearly violating SRP. It has 3 in our case: Content management, Display management and Persistence management. Beware of using “and then” to add more responsibility to your class or function and make ourselves believe that it has a single responsibility.
So now we have moved the extra responsibilities outside. Blog has only the info about the blog. “Printer” and “Persist” have been moved to their own module. Now if any of the three things have to change it does not affect the other places.
Open Closed Principle
Functionality must be open for extension but closed for modification
The purpose of the program is to find the remaining air in each of our vehicles. What happens when we want to add a new vehicle, say a cycle? We have to add another “if else” and modify the “findAirLeft” function. This does not end here. This “if else” ladder will be repeated at all the places where we want to handle some logic based on the vehicle type. As you can see we cannot extend the program without modification and for a small change we might have to change a lot of code.
Now if you see our refactored code, there is no “if else” ladder and we have an abstract class Vehicle with findAir method which all our vehicles implement. If we want to add another vehicle(extend) we need not touch the existing functionality(no modification).
Liskov’s Substitution Principle
You should be able to replace a super class with a sub class anywhere in the program without any unexpected behaviour
If we look at the example, we have “List” which stores a number of values. Now we want to implement “Stack”, we see that we already have a class storing a number of values, we decide to reuse the functionality, we extend the List class and override get and put methods to follow LIFO(this is how a stack works).
What could possibly go wrong? Take a look at line number 26. What happens when we pass a Stack object as a List to some other module. Somebody will use it like a List and everything will go for a toss because List and Stack don’t operate in the same way.
Whenever we are inheriting we need to make sure that it is replaceable. Also if we just need some functionality of another class, it is not mandatory to do inheritance, we can rather use composition and be happy.
Still we are re-using the “List” functionality. But now we are not inheriting. In this case no one will get hurt as there is no confusion.
Interface Segregation Principle
Create simple, focused interfaces instead of bloated big ones
What is the problem with the above piece of code. The interface clock defines all the functions of a clock. The problem is that Alarm Clock does not need all the functions, it needs only “set alarm” and “get alarm”, but it is forced to implement other unnecessary functions, either by returning null or leaving the function empty.
The idea is to create smaller and focussed interfaces instead of big bloated ones.
Dependency Inversion Principle
A class should not depend on another concrete class, instead it should depend on an abstraction
The problem with this class is that when we decide to switch to another form of storage for example say a SQL database or if FileWriter changes some of its APIs this class needs to change. BookDB class depends on the concrete FileWriter class. Tight coupling is never a good idea.
So we have pulled out an interface with our “save” method signature and FilePersist module implements it. Now the BookDB class depends on an abstraction and not directly on the FilePersist concrete implementation. With this change lower level module changes won’t affect BookDB and also we can easily switch the persistence method by injecting some other implementation of BookPersist interface(loose coupling).
If you observe carefully “Open Closed Principle” and “Dependency Inversion” go hand in hand. By following “Dependency Inversion” we can avoid violating “Open Closed Principle”.
In real world, probably we will end up violating some principles, let us be conscious of it and take a stance. For example, we violate a principle so that clients using our module won’t need to violate any.
SOLID is just one of many principles for writing “Clean Code”. There are others like KISS, DRY, YAGNI, etc.. We can talk about them in a later post. Please leave your feedback in the comments.