SOLID Principles
Refresher with sample code examples.
Introduction
SOLID is a software development principle that is guidelines to follow when building software to make it easier to scale and maintain. They were made famous by Robert C. Martin (known as Uncle Bob).
These principles outline best practices for designing software while considering the project’s long-term maintenance and expansion. Adopting these techniques can also help one avoid code smells, restructure one's code, and design Agile or adaptive software.
SOLID stands for:
- S — Single Responsibility Principle (SRP)
- O — Open-Closed Principle (OCP)
- L — Liskov’s Substitution Principle (LSP)
- I — Interface Segregation Principle (ISP)
- D — Dependency Inversion Principle (DIP)
SOLID principles complement each other and work together to achieve the common purpose of well-designed software.
Single Responsibility Principle (SRP)
Every software component should have one and only one responsibility.
Here component can be either a Java class, a method, or a module. If we consider a Swiss knife and a regular knife, a traditional knife adheres to the SRP because it has only one responsibility.
Cohesion
Cohesion is how various parts of a software component are related. Higher cohesion helps to attain better adherence to the Single Responsibility Principle
Below Circle class’s calculateArea() method and calculatePerimeter() method are highly cohesive, whereas it has low cohesion with drawCircle() and fillCircle() methods.
public class Circle {
private int radius;
public int calculateArea(){}
public int calculatePerimeter(){}
public void drawCircle(){}
public void fillCircle(){}
}
Therefore, the methods need to be put in different classes as below.
// Responsibility - Measurements of the Circlepublic class Circle {
private int radius;
public int calculateArea(){}
public int calculatePerimeter(){}
}
// Responsibility - Rendering the circlepublic class CircleUI {
public void drawCircle(){}
public void fillCircle(){}
}
Coupling
Coupling is defined as the level of interdependency between various software components. Loose coupling helps attain better adherence to the single responsibility principle.
Uncle Bob’s latest definition of SRP
Every software component should have one and only one reason to change.
More reasons to change → More changes in the future → More number of bugs → More money spent.
Open-Closed Principle (OCP)
Software components should be closed for modification but open for extension.
Closed for modification
New features are getting added to the software component; you should NOT have to modify the existing code.
Open for extension
A software component should be extendable to add a new feature or to add new behavior to it.
If we do not follow the open-closed principle, one needs to modify the existing code, and then the QE team needs to test the new and old features with complete regression. It could introduce bugs into the current feature as well.
Liskov’s Substitution Principle (LSP)
Objects should be replaceable with their subtypes without affecting the correctness of the program.
Unimplemented methods are almost always indicated as a design flaw. There are two ways to solve this problem:
- Breaking the hierarchy
- Tell, don’t ask.
Breaking the hierarchy
Traditionally inheritance is approached using the “Is-A” way of thinking. Liskov wants to move away from the “Is-A” method of creating interfaces.
Formula 1 racing car is a type of car. So, one should be able to extend car class when implementing Formula 1 car.
public class Car {
public double getCabinWidth() {
// Implementation
}
}
public class F1Car extends Car {
@Override
public double getCabinWidth() {
// UNIMPLEMENTED
}
public double getCockpitWidth() {
// Return Cockpit width
}
}
public class CarUtils {
public static void main(String [] args) {
Car Audi = new Car();
Car Merc = new Car();
Car FerrariF1 = new F1Car();
List <Car> cars = new ArrayList<>();
cars.add(Audi);
cars.add(Merc);
cars.add(FerrariF1);
for(Car car : cars) {
car.getCabinWidth();
}
}
}
The above code snippet will not work because F1Car doesn’t have the getCabinWidth( ) method implemented. It only has getCockpitWidth(). This fails Liskov’s test. This problem can be solved by breaking the hierarchy by introducing a Vehicle interface with the more abstract getInteriorWidth() method.
public interface Vehicle {
public double getInteriorWidth();
}
public class Car implements Vehicle {
private double getCabinWidth() {
// Implementation
}
@Override
public double getInteriorWidth() {
return this.getCabinWidth();
}
}
public class F1Car implements Vehicle{
private double getCockpitWidth() {
// Return Cockpit width
}
@Override
public double getInteriorWidth() {
return this.getCockpitWidth();
}
}
Tell, don’t ask
Below code snippet asks for the object type. Instead it should tell the method to get the interiorWidth(). If one can refactor as the code above, he/she can adhere the Liskov’s substitution principle.
public class CarUtils {
public static void main(String [] args) {
Car Audi = new Car();
Car Merc = new Car();
Car FerrariF1 = new F1Car();
List <Car> cars = new ArrayList<>();
cars.add(Audi);
cars.add(Merc);
cars.add(FerrariF1);
for(Car car : cars) {
if (car instanceof F1Car) {
((F1Car) car).getCockpitWidth();
} else {
car.getCabinWidth();
}
}
}
}
Interface Segregation Principle (ISP)
No client should be forced to depend on methods it does not use
The below code snippet has a lot of unimplemented methods.
public interface MultiFuncMachine {
public void print();
public void scan();
public void copy();
public void fax();
public void reset();
}
public class Scanner implements MultiFuncMachine{
@Override
public void print() {
// UNIMPLEMENTED
}
@Override
public void scan() {
}
@Override
public void copy() {
// UNIMPLEMENTED
}
@Override
public void fax() {
// UNIMPLEMENTED
}
@Override
public void reset() {
}
}
The solution to this problem is to segregate the interfaces as below.
public interface IScan {
public void scan();
}
public class Scanner implements IScan{
@Override
public void scan() {
}
}
Techniques to identify violations of ISP
- Fat interfaces
- Interfaces with low cohesion
- Empty method implementations
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level methods. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
The above picture shows that both high-level method ProductCatalog and low-level method SQLProductRepository are dependent on abstraction — ProductRepository. Also, it shows that details depend on the abstraction.
Dependency Injection
Ideally, productCatalog above doesn’t need to worry about when and where to instantiate the object. Therefore, the main program will inject the dependency into productCatalog instead of productCatalog instantiating the dependency. The injection can be done using the constructors.
Inversion of Control (IOC)
This is not a part of the dependency inversion principle. However, it is closely related to it. Usually, the dependency injection is done through the main control thread. We need to take the dependency injection to a separate thread from the main control thread. This can be achieved through frameworks. Spring framework does it with Spring IoC Container.
This covers the concept and rationale behind the SOLID principles.
Thanks for reading.
Happy Coding!