SOLID Principle

The Single Responsibility Principle (SRP) is a principle in object-oriented design that states that a class should have only one reason to change. In other words, a class should have only one responsibility.

For example:

              class Employee {

              public Pay calculatePay() {...}

              public void save() {...}

              public String describeEmployee() {...}

              }

Here we have pay calculation logic with database logic and reporting logic all mixed up within one class. If you have multiple responsibilities combined into one class, it might be difficult to change one part without breaking others.

Mixing responsibilities also makes the class harder to understand and harder to test, decreasing cohesion. The easiest way to fix this is to split the class into three different classes, with each having only one responsibility: database access, calculating pay, and reporting, all separated. 

Open to Extension but colse to modification.

Consider the below method of the class interestCalculation:

   public class interestCalculation {

    public double calculateInterest(Bank b) {

        if (b instanceof Sbi) {

            return v.getValue() * 0.8;

        if (b instanceof Pnb) {

            return v.getValue() * 0.5;

    }

}

Suppose we now want to add another subclass called Hdfc. We would have to modify the above class by adding another if statement, which goes against the Open-Closed Principle.

A better approach would be for the subclasses Sbi and Hdfc to override the calculateIntrest method:

  public class Bank {

    public double calculateInterest() {...}

}

public class Sbi extends Bank {

    public double calculateInterest() {

return this.getValue() * 0.8;

}

public class Hdfc extends Bank{

    public double calculateInterest() {

        return this.getValue() * 0.9;

}

Adding another Bank type is as simple as making another subclass and extending from the Bank class.


(LSP) is a fundamental principle in object-oriented programming that states that if a program is using a child class, it should be able to use any of its parent classes without causing any issues or unexpected behavior.

Consider a typical example of a Square derived class and Rectangle base class:

public class Rectangle {

    private double height;

    private double width;

    public void setHeight(double h) { height = h; }

    public void setWidht(double w) { width = w; }

    ...

}

public class Square extends Rectangle {

    public void setHeight(double h) {

        super.setHeight(h);

        super.setWidth(h);

    }

    public void setWidth(double w) {

        super.setHeight(w);

        super.setWidth(w);

    }

}

The above classes do not obey LSP because you cannot replace the Rectangle base class with its derived class Square. The Square class has extra constraints, i.e., the height and width must be the same. Therefore, substituting Rectangle with Square class may result in unexpected behavior.

Suppose there’s an interface for vehicle and a Bike class:

public interface Vehicle {

    public void drive();

    public void stop();

    public void refuel();

    public void openDoors();

}

public class Bike implements Vehicle {

    // Can be implemented

    public void drive() {...}

    public void stop() {...}

    public void refuel() {...}

    // Can not be implemented

    public void openDoors() {...}

}

As you can see, it does not make sense for a Bike class to implement the openDoors() method as a bike does not have any doors! To fix this, ISP proposes that the interfaces be broken down into multiple, small cohesive interfaces so that no class is forced to implement any interface, and therefore methods, that it does not need.

 

The Dependency Inversion Principle (DIP) is a principle in object-oriented design that states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

In the context of Java, this principle can be illustrated with the following example:

Let's say we have a `UserService` class that is responsible for managing user-related operations, such as creating users and retrieving user information. Initially, the `UserService` class directly depends on a specific implementation of a `UserRepository` to store and retrieve user data.

 public class UserService {

 private UserRepository userRepository;

    public UserService() {

     this.userRepository = new UserRepository();

    }

    public void createUser(User user) {

     // Perform some validation or business logic

     userRepository.save(user);

    }

    public User getUserById(String userId) {

     // Perform some validation or business logic

     return userRepository.findById(userId);

   }

}

In this implementation, the `UserService` has a direct dependency on the `UserRepository` class. This violates the Dependency Inversion Principle because the high-level `UserService` class is depending on the low-level `UserRepository` class.

To adhere to the Dependency Inversion Principle, we introduce an abstraction, such as an interface, to represent the operations that the `UserService` requires from the repository. Let's call it `IUserRepository`:

public interface IUserRepository {

    void save(User user);

    User findById(String userId);

}

Now, we modify the `UserService` class to depend on the abstraction instead of the concrete implementation:

public class UserService {

    private IUserRepository userRepository;

 

    public UserService(IUserRepository userRepository) {

     this.userRepository = userRepository;

    }

 

    public void createUser(User user) {

     // Perform some validation or business logic

     userRepository.save(user);

    }

 

    public User getUserById(String userId) {

     // Perform some validation or business logic

     return userRepository.findById(userId);

    }

}

With this change, the `UserService` class no longer directly depends on the concrete implementation of the `UserRepository` class but instead relies on the abstraction provided by the `IUserRepository` interface.

This inversion of dependency allows for greater flexibility and modularity. You can now easily provide different implementations of the `IUserRepository` interface without modifying the `UserService` class. For example, you can create a `MongoUserRepository` or a `MockUserRepository` that implement the `IUserRepository` interface and pass them to the `UserService` as needed.

By applying the Dependency Inversion Principle, we achieve a design where high-level modules (like `UserService`) and low-level modules (like `UserRepository`) depend on abstractions (such as `IUserRepository`) rather than concrete implementations. This promotes loose coupling and makes the system more flexible and maintainable.