SOLID Principles

Introduction

SOLID is a set of five design principles intended to make software designs more understandable, flexible, and maintainable. This was theorized by Robert C. Martin famously known as Uncle Bob in his paper published in the year 2000. 

1. Single responsibility principle (SRP)

Every software component should have one responsibility. Every software component should have one reason to change.

Cohesion – Cohesion refers to the degree to which the elements inside a module belong together.

Coupling – Coupling is the degree of interdependence between software modules. 

Perspective 
1. Method level
2. Class level
3. Package level
4. System design level
Benefits
1. Easy to change behaviour.
2. Very helpful in debugging. 
3. Easy to understand the code. 
4. Easy to write tests.
5. Less maintenance.

Below code does not follow SRP

class UserRegistrationService {
	
	public void registerUser(String username, String emailId) {
		// Create a sql connection
		// Insert the username into database
		// Close the connection
		// Send a welcome email to the user
	} 
}

Below code follows SRP

class DatabaseService {
	public void createConnection(){}
	public void insert(){}
	public void closeConnection(){}
}


class EmailService {
	public void sendEmail(){}
}


class UserRegistrationService {
	public void registerUser(String username, String emailId) {
		databaseService.insert(username, emailId);
		emailService.sendEmail(emailId);

	}
}
2. Open closed principle (OCP)

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. OCP encourages the use of polymorphism. Also a way to implement OCP is using strategy design pattern.

Below is an example for a bad design

interface Operation{}

class Addition implements Operation {
	int a;
	int b;
	int result;
	// Constructor, getters and setters
}

class Subtraction implements Operation {
	int a;
	int b;
	int result;
	// Constructor, getters and setters
}

class Calculator {
	public void calculate(Operation operation) {
		// Null checks
		if(operation instanceof Addition) {
			Addition addition = (Addition) operation;
			addition.setResult(addition.getA() + addition.getB());
		} else if(operation instanceof Subtraction) {
			Subtraction subtraction = (Subtraction) subtraction;
			subtraction.setResult(subtraction.getA() - subtraction.getB());
		}
	}
}

Polymorphism

1. Method overloading

void add(int a, int b)
void add(int a, int b, int c)

2. Method overriding

interface Operation {
	void perform();
}

class Addition implements Operation {
	@Override
	void perform() {
		// Some code
	}
}

class Subtraction implements Operation {
	@Override
	void perform() {
		// Some code
	}
}

Below is an example of design following OCP

interface Operation {
	void perform();
}

class Addition implements Operation {
	int a;
	int b;
	int result;
	// Constructor, getters and setters

	public void perform() {
		result = a + b;
	}
}

class Subtraction implements Operation {
	int a;
	int b;
	int result;
	// Constructor, getters and setters

	public void perform() {
		result = a - b;
	}
}

class Multiplication implements Operation {
	int a;
	int b;
	int result;
	// Constructor, getters and setters

	public void perform() {
		result = a * b;
	}
}

class Calculator {
	public void calculate(Operation operation) {
		// Null checks
		operation.perform();
	}
} 

Benefits
Suppose clients are using existing class and if we modify that class there is a chance of introduction of bugs in the existing features. So it is better to create a new class so that existing features are untouched.

3. Liskov substitution principle (LSP)

LSP states that if we substitute a superclass object reference with an object of any of its subclasses, the program should not break.

Correct substitution:

interface Fruit {
	void eat();
} 

class Apple implements Fruit {
	@Override
	void eat() {
		// Eat apple
	}
}

class Orange implements Fruit {
	@Override
	void eat() {
		// Eat orange
	}
}

Wrong substitution: Duck test

interface Bird {
	void speak();
}

class Duck implements Bird {
	@Override
	void speak() {
		// Duck quacks
	}

	void addBattery(){}
}

Another Wrong substitution: IS-A trick – A square is a rectangle

class Rectangle {
	int length;
	int breadth;
	// Getters

	public void setBreadth(int breadth) {
		this.breadth = breadth;
	}

	public void setLength(int length) {
		this.length = length;
	}

	int getArea() {
		return length * breadth;
	}
}

class Square extends Rectangle {
	@Override
	public void setBreadth(int breadth) {
		super.setBreadth(breadth);
		super.setLength(breadth);
	}

	@Override
	public void setLength(int length) {
		super.setBreadth(length);
		super.setLength(length);
	}	
}
Benefits
1. It will avoid confusing the clients while using our code.
2. Future developers who use the code will not get confused.
4. Interface segregation principle (ISP)

ISP states that no client should be forced to depend on methods it does not use.

 

Example violating ISP

interface MediaPlayer {
	void playAudio();
	void playVideo();
}

 

Correct use of ISP

interface AudioPlayer {
	void playAudio();
}

interface VideoPlayer {
	void playVideo();
}

class VLC implements AudioPlayer,VideoPlayer {
	
}

class Winamp implements AudioPlayer {
	
}

 

Benefit:

A client will use interfaces only it wants. So it will create less bugs.

5. Dependency inversion principle (DIP)

Robert C. Martin’s definition of the Dependency Inversion Principle consists of two parts:

1. High-level modules should not depend on low-level modules. Both should depend on abstractions.

2. Abstractions should not depend on details. Details should depend on abstractions.

Example

interface Car {
	List<Seat> getSeats(); 
}

interface Seat {}

class AudiR8Car implements Car {
	List<Seat> seats;

	@Override
	List<Seat> getSeats() {
		// return 2 seats;
	}
}

class LeatherSeat implements Seat {
	
} 

Benefit:

Loose coupling of the code

Additional Concepts
1. Dependency Injection (DI)

Dependency injection is a technique in which an object receives other objects that it depends on.

Types of Injections

1. Constructor Injection

2. Property Injection

3. Method Injection 

interface Car {
	void setTheSeats(List<Seat> seats); 
}

class AudiR8Car implements Car {
	List<Seat> seats;

	// Constructor injection
	AudiR8Car(List<Seat> seats) {
		this.seats = seats;
	}

	// Setter injection
	void setSeats(List<Seat> seats) {
		this.seats = seats;
	}

	// Method injection
	@Override
	void setTheSeats(List<Seat> seats) {
		this.seats = seats;
	}
}
2. Inversion of Control (IoC)

IoC inverts the flow of control as compared to traditional control flow.

Traditional flow:

class TextEditor {

	SpellChecker spellChecker;

	public TextEditor() {
        this.spellChecker = new SpellChecker();
    }
}
 IoC flow:

public class TextEditor {

    SpellChecker spellChecker;

    public TextEditor(SpellChecker spellChecker) {
        this.spellChecker = spellChecker;
    }
}
Differences

Dependency Injection is an implementation technique for populating instance variables of a class.

Dependency Inversion principle recommends that classes should only have direct relationships with high-level abstractions. It uses dependency injection to achieve it.

Inversion of Control inverts the flow of control as compared to traditional control flow. It uses dependency injection to do it.

SOLID Principles Summary

1. Single responsibility principles states that each class should have a single responsibilily. 

2. Open closed principle states that software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. It encourages use of polymorphism. 

3. Liskov substitution principle states that if we substitute a superclass object reference with an object of any of its subclasses, the program should not break. LSP says use polymorphism but with caution. Think from the client’s perspective.

4. Interface segregation principle states let there be many interfaces but no client should be forced to depend on methods it does not use. 

5. Dependency Inversion principle 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.

Let's connect on LinkedIn

© Copyright CompleteDesignInterviewCourse.com