Tuesday, June 1, 2021

SOLID principles

In object-oriented programming languages, the classes are the building blocks of any application. If these blocks are not strong, the the application is going to face a tough time in the future.

Poorly designed applications can cause difficult situations when the application scope goes up.

A set of well designed and written classes can speed up the coding process.

In this tutorial, We will learn the SOLID principles which are 5 most recommended design principles, that we should keep in mind while writing our classes.


1) Single Responsibility Principle (SRP)

  • The single responsibility principle states that "One class should have one and only one responsibility."
  • Implementation of multiple functionalities in a single class can mess up the code and if any modification is required may affect the whole class.
  • Let's understand the single responsibility principle through an example.

Suppose, Employee is a class having three methods namely printDetails(), calculateSalary(), and addEmployee(). Hence, the Employee class has three responsibilities to print the details of Employee, calculate Salary, and add employee. By using the single responsibility principle, we can separate these functionalities into three separate classes to fulfill the goal of the principle.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Employee {
	public void printDetails() {
		// functionality of the method
	}

	public void calculateSalary() {
		// functionality of the method
	}

	public void addEmployee() {
		// functionality of the method
	}
}

The above code snippet violates the single responsibility principle. To achieve single responsibility principle, we should implement a separate class that performs a single functionality only.  

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Employee {
	public void Employee() {
		// functionality of the method
	}
}

public class PrintEmployeeDetails {
	public void printDetails()
	{
		// functionality of the method
	}
}

public class Salary {
	public void calculateSalary() {
		// functionality of the method
	}
}
Here, we have achieved the goal of the single responsibility principle by separating the functionality into three separate classes.


2) Open-Closed Principle
  • The Open-Closed Principle (OCP) states that classes should be open for extension but closed for modification. 
  • "Open to extension" means that you should design your classes so that new functionality can be added as new requirements are generated. 
  • "Closed for modification" means that once you have developed a class you should never modify it, except to correct bugs.
  • Let's understand the single responsibility principle through an example.
  • Suppose, CoffeMachine is a class having two methods namely grindCoffee(), makeCoffee(). If you want additional feature to pour coffee in a mug, by making another subclass extending from the CoffeeMachine class we can fulfill the goal of the principle.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    public class CoffeeMachine {
        public void grindCoffee(){
            // Grind the coffee
        }
        public void makeCoffee(){
            // Brew the coffee
        }
    }
    
    public class PourIntoCup extends CoffeeMachine{
        public void pour(){
            // Pour into cup
        }
    }
    

3) Liskov Substitution Principle
  • Derived classes must be completely substitutable for their base classes.
  • As the name suggests, Liskov Substitution Principle prescribes substitutability of a class by its subclass. 
  • Java inheritance mechanism follows Liskov Substitution Principle.
  • Let's say we have a class Vehicle and its two sub-classes Car & Bus as shown below:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Vehical {
	void getSpeed() {
		
	}
	void getMilage() {
		
	}
}

class Car {
	void getSpeed() {
		
	}
	void getMilage() {
		
	}
}

class Bus {
	void getSpeed() {
		
	}
	void getMilage() {
		
	}
}

class UsingLiskov {
	public static void main(String[] args) {
		Vehical vehical = new Car(); 
		vehical.getSpeed();
		vehical = new Bus();
		vehical.getMilage();
	}
}

We can assign an object of type Car or Bus to a reference of type Vehicle. All the functionality in base class i.e. Vehicle, and is acquired by Bus and Car via inheritance, can be invoked on a reference of type Vehicle. 

This is exactly what the Liskov Substitution Principle also states – subtype objects can replace super type objects without affecting the functionality inherent in the super type. 

The classic example which violates Liskov Substitution Principle is explained below

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Car{
  void move( int meters ) {...}
  void refuel( int litters ) {...}
}

class ElectricCar extends Car
{
  void move( int meters ) {...}
  void refuel( int litters ) {...}
}

This violates Liskov Substitution Principle since an electric car is not fueled by litters metrics, but the contract of the Car class states that an electric car can.
This can be solved by applying better domain naming, a Car can be named PetrolPoweredCar and ElectricPoweredCar. Both classes can be more abstracted by extending from Vehicle base class.

4) Interface Segregation Principle

  • The principle states that the larger interfaces split into smaller ones. 
  • The implementation classes use only the methods that are required. 
  • We should not force the client to use the methods that they do not want to use.
  • Suppose, we have created an interface named Connection having four methods open(), close(), receive() and send().
1
2
3
4
5
6
public interface Connection {
	void open();
	void close();
	void receive();
	void send(byte[] data);
}

After we applied ISP, we ended up with two different interfaces, with each one representing one exact role.

1
2
3
4
5
6
7
8
9
public interface Channel {
    void receive();
    void send(byte[] data);  
}

public interface Connection {
    void open();
    void close();  
}

 5) Dependency Inversion Principle
  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend upon details; details should depend upon abstractions. 
  • This aims to reduce the coupling between the classes by introducing abstraction between the layer.
You go to a local store to buy something, and you decide to pay for it by using your debit card. So, when you give your card to the clerk for making the payment, the clerk doesn’t bother to check what kind of card you have given.

Even if you have given a Visa card, he will not put out a Visa machine for swiping your card. The type of credit card or debit card that you have for paying does not even matter; they will simply swipe it. 

So, in this example, you can see that both you and the clerk are dependent on the credit card abstraction and you are not worried about the specifics of the card.