Solid Principles

Sathish Gajendran
5 min readJan 20, 2023

--

SOLID is an acronym for the following five design principles, which improves the system flexibility, understandability, and maintainability. Also, the system can be easily extendable, independently deployable, scalable and has much more advantages in system design realm.

  • Single Responsibility Principle
  • Open Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

Let us understand each principle with an example.

Single Responsibility Principle

Consider a motorcycle engine, The main functionality of the engine is to convert the fuel into rotatory motion (i.e.) It is responsible for generating rotatory motion and it does not care about wheels, lights, horns etc.,

What happens, if the engine can control lights, that is completely meaningless. Right? This is how the single responsibility principle works.

A component or class in an application should do only one thing, if it does two or more things, it violates the single responsibility principle.

Code example:

class Employee { 
id: string;
name: string;

calculateBonus() {
// Bonus calculation
}
}

The above class violates the single responsibility principle. The above class calculate the bonus of the employees this depend on the type of the employees and some other constraints like pay type, country etc.

We can modify the above class as follows

class Employee { 
id: string;
name: string;
}

class EmployeeBonus {
employee: Employee;

constructor(employee: Employee) {
this.employee = employee;
}

calculate() {
// Bonus calculation
}
}

Open Closed Principle

To understand open closed principle, we can use hand and gloves as example. Consider you want to play a Cricket, so you use a Cricket gloves. Doctors use surgery gloves and baseball player use another type of gloves. Here, your hand is closed for modification and open for extension i.e. You cannot modify your hand to play a cricket or baseball instead you can use different variety of gloves to fulfil your needs.

A component or class in application should be open for extension and closed for modification.

class EmployeePayment { 
employee: Employee;

constructor(employee: Employee) {
this.employee = employee;
}

calculate() {
throw new Error("Not Allowed");
}
}

class BiWeeklyPayment extends EmployeePayment {
calculate() {
// payment calculation
}
}

class MonthlyPayment extends EmployeePayment {
calculate() {
// payment calculation
}
}

Here you can see, we are allowed to extend EmployeePayment class but not allowed to modify it. You are violating the open closed principle when you modify EmployeePayment class. This leads to a lot of problems, and it may affect the other derived classes.

Liskov Substitution Principle

We can use water tap as an example for understanding Liskov substitution principle. The functionality of water tap is to control the flow of water and all types of taps does the same operation. And we also have several types of taps like automatic taps, two-way taps, and taps with filters. We can use a tap based on our needs i.e. here one tap can be substituted by other, but it should perform basic operation i.e. controlling the water flow.

A Liskov substitution principle states that, A class or component of the application, which is derived from some base class is follows the principle when it can do operation more than the base class but not less than it. i.e. the base class can be substituted by the derived class.

class Tap { 
controlWaterflow() {
// basic water flow control
}
}

class automaticTap extends Tap {
checkSensor() {
// checks status of sensor and controls flow
}

controlWaterflow() {
const currentState = this.checkSensor();
// controls water flow
}
}

class TwoWayTap extends Tap {
controlOutlet1() {}
controlOutlet2() {}

controlWaterflow() {
const someConditon = "";
// controls water flow in outlet1 or outlet2
// this.controlOutlet1();
// this.controlOutlet2();
}
}

Interface segregation principle

We can use LEGO as an example for Interface segregation principle. LEGO is a piece of plastic, using which you can construct other objects or structures. Consider you have animal body parts as LEGO and there is a head LEGO, which is attached with horns, and you want to create a horse. So, you are joining the body parts LEGO pieces to create a horse. Finally, you got the horse but with horns.

Horse does not have horns, right? That is what the interface segregation principle deals with. Here, horn should be segregated from the head LEGO and unwanted in the horse body.

Interface segregation principle states that, class should not contain or depend on the methods which do not use, or the class should be segregated in such a way that it should not have variables and methods which is not required in derived class. Consider the following example

class Company { 
name: string;
projectDetails: { someData: string };
}

class ParkingSpaceManagement extends Company {
totalSpaces: number;
//some other info
}

Here, projectDetails field is unwanted in ParkingSpaceManagement class. So, it should be segregated as like as follows

class Company { 
name: string;
}

class CompanyProjects {
projectDetails: { someData: string };
}

class ParkingSpaceManagement extends Company {
totalSpaces: number;
//some other info
}

Dependency Inversion principle

For understanding Dependency inversion principle, we can use motorbike again. Consider a motor bike wheel, usually we have a mechanism to couple wheel and motorbike chassis, instead of directly connecting bike and wheel. This is because, motorbike is dependent of coupling mechanism instead of directly depend on wheel type. In future, the same motorbike may use different wheel. During that time, it is enough to modify the coupling mechanism i.e. instead of directly depending on wheel, the bike is dependent on coupling mechanism.

Here the dependency is inverted, this is how Dependency inversion principle works. A class or component should be depend on the interface rather than the class itself. This gives flexibility to adapt different variety of function.

Consider the following example, you designed the printing system that can print in 2D and 3D objects.

class Print2DObject { 
static print() {
// prints 2d objects
}
}

class Print3DObject {
static print() {
// prints 3d objects
}
}

class PrintingSystem {
print2D() {
Print2DObject.print();
// prints 2d Object
}

print3D() {
Print3DObject.print();
// prints 3d Object
}
}

In the above declaration, the PrintingSystem class directly depend on the Print2DObject and Print3DObject class i.e., if any modification we do to the printer class affect entire system. It can be designed in the following way

class PrintStrategy { 
print() {}
}

class Print2DObject extends PrintStrategy {
print() {
// prints 2d objects
}
}

class Print3DObject extends PrintStrategy {
print() {
// prints 3d objects
}
}

class PrintingSystem {
printStrategy: PrintStrategy;

constructor(printStrategy: PrintStrategy) {
this.printStrategy = printStrategy;
}
print() {
this.printStrategy.print();
}
}

const print2dObject = new PrintingSystem(new Print2DObject());
print2dObject.print();

const print3dObject = new PrintingSystem(new Print3DObject());
print3dObject.print();

const printAnything = new PrintingSystem(new PrintAnything());
printAnything.print();

Here, the PrintingSystem is dependent on the strategy rather than the printing type itself. This gives flexibility to add the various kinds of strategy to the system and makes system more useable and powerful.

https://sathishgajendran.github.io

--

--