DEV Community

Subhendu Mondal
Subhendu Mondal

Posted on

SOLID Principle in details.

Before I start, I would like to say thanks to Co-Author of this article Soumya Pal.

SOLID Principle

SOLID Principle is a widely used Software Design Pattern. There are five principles together form the SOLID principle. It was introduced by Robert C. Martin in 1988, which is 8 years before the Java 1st release. Five principles are listed below

  • S - Single Responsibility Principle
  • O - Open for Extension, Closed for Modification Principle
  • L - Liskov Substitution Principle
  • I - Interface Segregation Principle
  • D - Dependency Inversion Principle

Single Responsibility Principle

S stands for Single Responsibility Principle. Which means a single module can have one specific responsibility rather have a lot.
We generally know a God Class which means, a class know lots of things, this approach violates the Single Responsibility Principle.
Let's Take an example, We need a HealthInsurance software which can approve claims which have been made. Now we check the below class

package org.example.solidprinciple

class HealthInsurance {

    /**
    * This function will verify that, the documents provide is
    * all good or not good.
    * @return Boolean
    */
    public Boolean isValidClaims() {
        // Function defination goes here.
        return true;
    }    

    /**
    * This function will check wheather claims are valid or not the 
    * approved the claims.
    * @return Boolean
    */
    public Boolean approve() {
        if(this.isValidClaims()){
            return true;
        }

        return false;
    }

}

Here we can see that, a Single HealthInsurance class handling the verification of claims as well as approval of claims too, here the Single Responsibility Principle violate, a class should
not contain more than one responsibility with own. Now let's modify the code.

package org.example.solidprinciple

class HealthInsurance {

    /**
    * This function will verify that, the documents provide is
    * all good or not good.
    * @return Boolean
    */
    public Boolean isValidClaims() {
        // Function defination goes here.
        return true;
    }

}


class InsuranceApprovalManager {

    private HealthInsurance healthInsurance;

    InsuranceApprovalManager (HealthInsurance healthInsurance) {
        this.healthInsurance = healthInsurance;
    }

    /**
    * This function will check wheather claims are valid or not the 
    * approved the claims.
    * @return Boolean
    */
    public Boolean approve() {
        if(healthInsurance.isValidClaims()){
            System.out.println("Insurance Claims Successfully Completed.");
            return true;
        }

        System.out.println("Insurance Claims Failed.");
        return false;
    }
}

Now we have distributed the responsibility in two class, one HealthInsurance which will verify the claim and InsuranceApprovalManager will be approved after confirming the claims is good or not good. This is now following the Single Responsibility Principle.

Open for Extension, Closed for Modification Principle

The Second Principle of SOLID property is Open for extension but closed for modification. The name itself explains the concept. One module needs to be designed in such a way that, we can achieve different behaviour without modifying the code. STOP but how? Let's Take the same example as we have seen above.

package org.example.solidprinciple

class HealthInsurance {

    /**
    * This function will verify that, the documents provide is
    * all good or not good.
    * @return Boolean
    */
    public Boolean isValidClaims() {
        // Function defination goes here.
        return true;
    }

}


class InsuranceApprovalManager {

    private HealthInsurance healthInsurance;

    InsuranceApprovalManager (HealthInsurance healthInsurance) {
        this.healthInsurance = healthInsurance;
    }

    /**
    * This function will check wheather claims are valid or not the 
    * approved the claims.
    * @return Boolean
    */
    public Boolean approve() {
        if(healthInsurance.isValidClaims()){
            System.out.println("Insurance Claims Successfully Completed.");
            return true;
        }

        System.out.println("Insurance Claims Failed.");
        return false;
    }
}

In this example, InsuranceApprovalManager class dependent on HealthInsurance, as it's taking an object of HealthInsurance in the constructor. Now if another insurance say LifeInsurance comes in the picture, how Insurance Manager will approve the claim, then we need to modify the code. Here we violating the second principle because we are modifying the code. Let's see how we could write the code so that we don't need to modify the code to achieve the behaviour change.

package org.example.solidprinciple

interface Insurance {
    public Boolean isValidClaims();
}

class HealthInsurance implements Insurance{

    /**
    * This function will verify that the documents provide is
    * all good or not good.
    * @return Boolean
    */

    @Override
    public Boolean isValidClaims() {
        // Function defination goes here.
        return true;
    }

}

class LifeInsurance implements Insurance{

    /**
    * This function will verify that the documents provide is
    * all good or not good.
    * @return Boolean
    */

    @Override
    public Boolean isValidClaims() {
        // Function defination goes here.
        return true;
    }

}

class InsuranceApprovalManager {

    private Insurance insurance;

    InsuranceApprovalManager (Insurance insurance) {
        this.insurance = insurance;
    }

    /**
    * This function will check wheather claims are valid or not the 
    * approved the claims.
    * @return Boolean
    */
    public Boolean approve() {
        if(insurance.isValidClaims()){
            System.out.println("Insurance Claims Successfully Completed.");
            return true;
        }

        System.out.println("Insurance Claims Failed.");
        return false;
    }
}

Here we defined an interface called Insurance, and implement it in HealthInsurance, LifeInsurance class, now check the InsuranceApprovalManager constructor

InsuranceApprovalManager (Insurance insurance) {
    this.insurance = insurance;
}

taking a reference type of Insurance, So that, if other Insurance types added in future we can extend it and use it. No Need for modification of the current code structure.

Liskov Substitution Principle

This principle was introduced by Barbara Liskov in 1988. It states that a module S is a subtype of T then object of T can be replaced by the object of S with altering its property. That means reference class must be able to use object derived class without knowing it. Let's take an example below,

package org.example.solidprinciple

interface Insurance {
    public Boolean isValidClaims();
    public Boolean doctorVerification();
}

class HealthInsurance implements Insurance{

    /**
    * This function will verify that the documents provide is
    * all good or not good.
    * @return Boolean
    */

    @Override
    public Boolean isValidClaims() {
        // Function defination goes here.
        if(this.doctorVerification()) {
            return true;
        }
        return false;
    }

    /**
    * The Claims will check by the doctor whether it is approvable or not.
    * @return Boolean
    */

    @Override
    public Boolean doctorVerification() {
        //Function defincation goes here
        return true;
    }

}

class InsuranceApprovalManager {

    private Insurance insurance;

    InsuranceApprovalManager (Insurance insurance) {
        this.insurance = insurance;
    }

    /**
    * This function will check wheather claims are valid or not the 
    * approved the claims.
    * @return Boolean
    */
    public Boolean approve() {
        if(insurance.isValidClaims()){
            System.out.println("Insurance Claims Successfully Completed.");
            return true;
        }

        System.out.println("Insurance Claims Failed.");
        return false;
    }
}

Here check this HealthInsurance class isValidClaims() this function depends on doctorVerification(). For particular this example everything is fine. Now let's check the below example,

package org.example.solidprinciple

interface Insurance {
    public Boolean isValidClaims();
    public Boolean doctorVerification();
}

class CarInsurance implements Insurance{

    /**
    * This function will verify that the documents provide is
    * all good or not good.
    * @return Boolean
    */

    @Override
    public Boolean isValidClaims() {
        // Function defination goes here.
        if(this.doctorVerification()) {
            return true;
        }
        return false;
    }

    /**
    * The Claims will check by the doctor whether it is approvable or not.
    * @return Boolean
    */

    @Override
    public Boolean doctorVerification() {
        //Function defincation goes here
        return true;
    }

}

class InsuranceApprovalManager {

    private Insurance insurance;

    InsuranceApprovalManager (Insurance insurance) {
        this.insurance = insurance;
    }

    /**
    * This function will check wheather claims are valid or not the 
    * approved the claims.
    * @return Boolean
    */
    public Boolean approve() {
        if(insurance.isValidClaims()){
            System.out.println("Insurance Claims Successfully Completed.");
            return true;
        }

        System.out.println("Insurance Claims Failed.");
        return false;
    }
}

Here we are using CarInsurance, so doctorVerification() is not needed at all, but we need to implement the function. This situation violating the 3rd Principle of the stack. How we can overcome this situation, let's check the below example.

package org.example.solidprinciple

interface Insurance {
    public Boolean isValidClaims();
}

interface DoctorVerifiableInsurance extends Insurance {
    public Boolean isValidClaims();
    public Boolean doctorVerification();

} 

interface EngineerVerifiableInsurance extends Insurance {
    public Boolean isValidClaims();
    public Boolean engineerVerification();

} 

class HealthInsurance implements DoctorVerifiableInsurance{

    /**
    * This function will verify that the documents provide is
    * all good or not good.
    * @return Boolean
    */

    @Override
    public Boolean isValidClaims() {
        // Function defination goes here.
        if(this.doctorVerification()) {
            return true;
        }
        return false;
    }

    /**
    * The Claims will check by the doctor whether it is approvable or not.
    * @return Boolean
    */

    @Override
    public Boolean doctorVerification() {
        //Function defincation goes here
        return true;
    }

}

class CarInsurance implements EngineerVerifiableInsurance{

    /**
    * This function will verify that the documents provide is
    * all good or not good.
    * @return Boolean
    */

    @Override
    public Boolean isValidClaims() {
        // Function defination goes here.
        if(this.engineerVerification()) {
            return true;
        }
        return false;
    }

    /**
    * The Claims will check by the doctor whether it is approvable or not.
    * @return Boolean
    */

    @Override
    public Boolean engineerVerification() {
        //Function defincation goes here
        return true;
    }

}

class InsuranceApprovalManager {

    private Insurance insurance;

    InsuranceApprovalManager (Insurance insurance) {
        this.insurance = insurance;
    }

    /**
    * This function will check wheather claims are valid or not the 
    * approved the claims.
    * @return Boolean
    */
    public Boolean approve() {
        if(insurance.isValidClaims()){
            System.out.println("Insurance Claims Successfully Completed.");
            return true;
        }

        System.out.println("Insurance Claims Failed.");
        return false;
    }
}

Here we have defined two more interfaces (EngineerVerifiableInsurance, DoctorVerifiableInsurance) extending the base interface, and implemented the class HealthInsurance, CarInsurance. Now we have the correct flow where we don't need to implements those methods which not belongs to the particular Insurance category.

Interface Segregation Principle:

This principle is the first principle that applies to Interfaces instead of classes in SOLID and it is similar to the single responsibility principle. It states that “do not force any client to implement an interface which is irrelevant to them“. Here your main goal is to focus on avoiding fat interface and give preference to many small client-specific interfaces. You should prefer many client interfaces rather than one general interface and each interface should have a specific responsibility.

Lets take an example:

package org.example.solidprinciple

interface Insurance {
    public Boolean isValidClaims();
    public Boolean verification();
    public Boolean lifeInsurance();
    public Boolean healthInsurance();
    public Boolean fireInsurance();
}

class Insuranceclaim implements Insurance{


    @Override
    public Boolean lifeInsurance() {
        if(isValidClaims()){
            return true;
        }
        return false;
    }


    /**
    * This function will verify that the documents provide is
    * all good or not good.
    * @return Boolean
    */
    @Override
    public Boolean isValidClaims() {
        if (verification()){
            return true;
        }
        return false;
    }

    @Override
    public Boolean verification() {
        return true;
    }

    @Override
    public Boolean healthInsurance() {
        return null;
    }

    @Override
    public Boolean fireInsurance() {
        return null;
    }
}


class InsuranceApprovalManager{

    private Insuranceclaim insurance;

    InsuranceApprovalManager(Insuranceclaim insurance){
        this.insurance = insurance;
    }

    /**
    * This function will check wheather claims are valid or not the 
    * approved the claims.
    * @return Boolean
    */
    public void approve(){
        if(insurance.lifeInsurance()){
            System.out.println("Insurance amount processed successfully");
        }
        else {
            System.out.println("Insurance amount not approved");
        }
    }
}

In the above example you can see that "InsuranceClaim" needs to provide implementation of "healthInsurance()" and "fireInsurance()" methods even though it does not require them. This is a violation of the Interface Segregation Principle. Such violations affect code readability and confuse programmers. So how to resolve this issue? Here is the solution by segregating the interface into multiple interfaces each for specific behaviour. Take a look at the example below...


package org.example.solidprinciple

interface Insurance {
    public Boolean isValidClaims();
    public Boolean verification();
}

interface LifeInsurace{
    public Boolean lifeInsurance();
}
interface HealthInsurance{
    public Boolean healthInsurance();
}
interface FireInsurance{
    public Boolean fireInsurance();
}

class Insuranceclaim implements  LifeInsurace,Insurance{


    @Override
    public Boolean lifeInsurance() {
        if(isValidClaims()){
            return true;
        }
        return false;
    }

    /**
    * This function will verify that the documents provide is
    * all good or not good.
    * @return Boolean
    */
    @Override
    public Boolean isValidClaims() {
        if (verification()){
            //
            return true;
        }
        return false;
    }

    @Override
    public Boolean verification() {
        return true;
    }

}


class InsuranceApprovalManager{

    private Insuranceclaim insurance;

    InsuranceApprovalManager(Insuranceclaim insurance){
        this.insurance = insurance;
    }

    /**
    * This function will check wheather claims are valid or not the 
    * approved the claims.
    * @return Boolean
    */
    public void approve(){
        if(insurance.lifeInsurance()){
            System.out.println("Insurance amount processed successfully");
        }
        else {
            System.out.println("Insurance amount not approved");
        }
    }
}

In the above example, "Insurance" interface segregated into four interfaces namely LifeInsurance, HealthInsurance, FireInsurance and the "Insurance" interface itself. Each of which is responsible for a specific behaviour and it is also seen that when the "InsuranceClaim" implements "LifeInsurance" and "Insurance" the other two interfaces need not be present there. So the modified code is more readable and lesser prone to modification due to the segregation of interfaces.

Dependency Inversion Principle

The general idea of this principle is as simple as it is important: High-level modules, which provide complex logic, should be easily reusable and unaffected by changes in low-level modules, which provide utility features. To achieve that, you need to introduce an abstraction that decouples the high-level and low-level modules from each other.

Based on this idea, 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.

package org.example.solidprinciple

class Insurance{

    public Boolean isValidClaims(){
        return true;
    }

    /**
    * The Claims will check by the doctor whether it is approvable or not.
    * @return Boolean
    */
    public Boolean approval(){
        if(DoctorvVerification()){
            return true;
        }
        return false;
    }


    public Boolean DoctorvVerification(){
        if(isValidClaims()){
            return true;
        }
        return false;
    }
}

class ManageHeathInsuranceClaim {

    private Insurance insurance;

    ManageHeathInsuranceClaim(Insurance insurance){
        this.insurance = insurance;
    }

    /**
    * This function will check wheather claims are valid or not the 
    * approved the claims.
    * @return Boolean
    */
    public void approved(){
        if(insurance.approval()){
            System.out.println("Insurance Claim processed successfully");
        }
        System.out.println("Claim couldn't be processed");
    }
}

In the above example, it is seen that the ManageHealthInsuranceClaim depends on the Insurance now which is a high-level module depends on the low-level module. But it is violating the first property of the Dependency Inversion Principle. Also If anyone wants to add some other insurance, says PropertyInsurance, for that the Insurance class need to modify. To solve this we need to implement an interface. Take a look at the example given below...


package org.example.solidprinciple

interface Insurance{
    public Boolean isValidClaims();
    public Boolean approval();
}

class HealthInsuranceManager implements Insurance {

    public Boolean isValidClaims() {
        return true;
    }

    public Boolean approval() {
        if (doctorVerification()) {
            return true;
        }
        return false;
    }


    /**
    * The Claims will check by the doctor whether it is approvable or not.
    * @return Boolean
    */
    public Boolean doctorVerification() {
        if (isValidClaims()) {
            return true;
        }
        return false;
    }
}

class PropertyInsuranceManager implements Insurance{

   @Override
    public Boolean isValidClaims() {
        return true;
    }

    public Boolean approval(){
        if(proertyVerification()){
            return true;
        }
        return false;
    }


    /**
    * The Claims will check by the govt. whether it is approvable or not.
    * @return Boolean
    */
    public Boolean proertyVerification(){
        if(isValidClaims()){
            return true;
        }
        return false;
    }
}


class ManageInsuranceClaim{

    private Insurance insurance;

    ManageInsuranceClaim(Insurance insurance){
        this.insurance = insurance;
    }

    /**
    * This function will check wheather claims are valid or not the 
    * approved the claims.
    * @return Boolean
    */
    public void approved(){
        if(insurance.approval()){
            System.out.println("Insurance Claim processed successfully");
        }
        System.out.println("Claim couldn't be processed");
    }
}

In the modified code the High-level module in depending on the abstraction(interface Insurance) instead of the low-level module(HealthInsurancemanager, PropertyInsuranceManager) and also the low level depends on the abstraction that means anyone can add other insurance module and no need to modify the code. This code fully satisfies all the properties of the Dependency Inversion Principle.

Hope we can provide you in-depth knowledge on SOLID Property.

Top comments (0)