Coding is fun when we do it for fun. And Software development resembles a game when we are fond of challenges. Nevertheless, we all come across of time when it relates to a business. These days, it’s not only about innovation, it’s equally about sustenance. Therefore, when we talk about sustenance of an idea on the low level, we deal into maintaining applications in a continuous evolution pattern. A very well-known fact is 80% of time and effort which also means money is spent on maintenance of a product. Let us try to understand this fact.
To start with, I started visualizing the following use case: Say I purchased a bike for 1 lakh and I spend 4 lakhs on its maintenance over next 5 years. These numbers pushed me to uproot the cause of high maintenance of a software that is already been developed.
With a lot of research, I feel one of the major challenge with developers these days is Bad Code. Now the question is what is Bad Code? Okay! So before going ahead with the concept of Bad Code, let’s see the programming world with top level view.
With an approximate data, it was concluded that number of programmers are getting double every five years. That means a perpetual process of having half of the programmers in the world with less experience will always prevail. There are many complex implications to this, the major one being -callowness in code. And I quote here – “If your code just worked as per the business requirement, Congratulation! The worse code is ready”. Meaning thereby, that piece of code which will completely suffice the business logic might guarantees no quality in terms of free from rigidity and fragility- the two most common characteristics of a bad code. Something was missing in the code. We do not shoot an email the way we chat, because we adhere to the mailing etiquettes. Similarly, coding etiquettes is what I am talking here.
And as I reach into the gorge, it leads me to some of the basics in programming. Every Object-Oriented design talks about encapsulation, inheritance and polymorphism. But did they really come up with OO design?
Absolutely Not. They were in practice even before OO concept surfaced. So, what does OO came up with? For me, Object Oriented is all about managing the desired dependency in a selective way where it can avoid the bad coding practices – rigidity and fragility. Let’s define these two terms now: Rigidity means change in code at one place breaks the code at another place, and fragile code is that which has unwanted dependency for a small change. These two either completely or partially, marks a bad code.
Now if we understood this correctly it’s easy to digest why the estimations for a small change in functionality goes high; resulting loss of time and money.
So how can we reduce the maintenance cost effectively? This is where I encountered SOLID principles rightly proposed by Mr. Bob Martin, well known for his book ‘Clean Code’. He came up with some coding principles which will suggest us to write code in way that will basically help us organize the dependent modules and avoid bad code characteristics like rigidity, fragility and immobility. In fact, we will have a better maintainable code for a product.
The five principles:
Single Responsibility principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
talks very clearly on how we need to organize our OO design. Let’s talk about how each of these principles help us writing a better code.
Understanding SOLID principles
Single Responsibility Principle:
A class should have only one reason to change.
Each class or module should be very specific to its role. In the figure 1.1, a payroll application EmployeeService class is doing multiple operations like getting employee information from database, calculate reporting hours and also generating pay slip.
Fig 1.1 Code Illustration breaking Single Responsibility
So, we can implement single responsibility if we can put all three operations in separate classes as depicted in figure 1.2.
The different concern from a business point of view should be decoupled, so that any change in one of the module should not impact the other modules.
Fig 1.2 Code Illustration following Single Responsibility
A class, once completed should be open for extension but close for modification.
It means we should be able to add or modify the functionality of module without changing the base module. Let’s consider the same Employee scenario.
Fig 1.3 Code Illustration breaking Open/Closed Principle
Suppose employee class is completed and now a new type of employee joined, say temporary employee or intern. So, in this case we should not be modifying the employee class(Fig 1.3), instead we should inherit this employee class and provide custom behaviors to the new class (Fig. 1.4).
Fig 1.4 Code Illustration following Open/Closed Principle
Liskov Substitution principle:
Derived classes must be usable through the base class interface without the need for the user to know the difference.
For modules to adhere to LSP, the input parameters of any method in derived class should be contravariant. Contravariance, allow us to use a more generic type, where a more derived type is specified.
Say in the above example, we refactor the code further and made derived classes like below:
Fig 1.5 Code Illustration for LSP
We have a method in SalaryService class to calculate Salary of an employee. with input parameter specified as PermanentEmployee.
Fig 1.6 Code Illustration breaking Liskov Substitution principle
At any later point of time, If Company announced that we will have salary for temporary employees also, then we will need to have another method which will calculate salary for temporary employees. This is not very generic solution.
If we apply LSV here, Our Salary class method should be same and the input parameter type for the GeneratePayslip method should be the base class so that GeneratePayslip can be called for any child class of Employee base class.
Fig 1.7 Code Illustration following Liskov Substitution principle
Interface Segregation Principle:
Clients should not be forced to depend upon interfaces that they don’t use.
This means that a class should not be inheriting from unrelated interfaces that may not be needed. We can have explicit interfaces for different classes so that class inherit only the required interfaces and implement them.
In the above example, suppose I implement interface IEmployeeService with some methods as depicted below:
Fig 1.8 Code Illustration breaking Interface Segregation principle
Now each class inheriting IEmployeeService interface must implement all three methods.
A class EmployeeService which deals with crud operation on Employee may not be interested to operation like GeneratePayslip. And Salary class which implements GeneratePayslip may not be interested to deal with adding and editing operations.
As per ISP, interfaces should be segregated on basis of classes which will implement them. Unrelated operations should be placed in separate interfaces.
Fig 1.9 Code Illustration following Interface Segregation principle
Dependency Inversion Principle:
Two points that it covers are:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
In a general flow, we should not keep classes coupled in such a way that any change in low level modules (like business logic implementation) should lead to recompilation of the high-level modules (likes controller classes).
In below code snippet, Salary service is dependent on EmployeeService class which contains the employee information.
Fig 1.10 Code Illustration breaking Dependency inversion principle
Instead, we should be having a mechanism in place where the dependent modules should be injected. We can achieve dependency injection by constructor injection, parameter injection or implement any other inversion of control pattern to adhere with DIP. Below code is an example of constructor injection(Fig. 1.11).
Fig 1.11 Code Illustration following Dependency inversion principle
SOLID principles help us manage the dependency and structure the modules in such a way that we develop code keeping in mind the readability, usability, maintainability and portability of product. Just like how we practice a morning walk – some extra effort to daily life helps us keeping away from doctors, the same way if we practice these principles when we code, it helps in survival of product in long run.