SOLID Principles in Java

Photo by NeONBRAND on Unsplash

SOLID Principles in Java

In software engineering, SOLID is an acronym for 5 design principles intended to make software designs more understandable, flexible, robust, and maintainable. Adopting these practices can contribute to avoiding code smells too.

The 5 SOLID principles are:

  • S - The single-responsibility principle
  • O - The open-closed principle
  • L - The Liskov substitution principle
  • I - The interface segregation principle
  • D - The dependency inversion principle

Although the SOLID principles apply to any programming language, in further section I will be explaining each of them with examples written specifically in JAVA.

Single Responsibility Principle

This principle states that “a class should have only one reason to change” which means every class should have a single responsibility.

public class Vehicle {
    public void details() {}
    public double price() {}
    public void addNewVehicle() {}
}

Here the class has multiple reasons to change because the Vehicle class has three separate responsibilities: printing details, printing price, and adding a new vehicle to Database.

To achieve the goal of the single responsibility principle, we should implement a separate class that performs a single functionality only.

public class VehicleDetails {
    public void details() {}
}

public class CalculateVehiclePrice {
    public double price() {}
}

public class AddVehicle {
    public void addNewVehicle() {}
}

Open-Closed Principle

This principle states that “software entities (classes etc.) should be open for extension, but closed for modification”. This means without modifying anything in a class, it should be extendable.

Let's understand this principle with an example of a notification service

public class NotificationService{
    public void sendNotification(String medium) {
         if (medium.equals("email")) {}
    }
}

Here, if you want to introduce a new medium other than email, let's say send a notification to a mobile number then you need to modify the source code in NotificationService class.

So to overcome this you need to design your code in such a way that everyone can reuse your feature by extending it and if they need any customization they can extend the class and add their feature on top of it.

You can create a new interface like:

public interface NotificationService {
    public void sendNotification(String medium);
}

Email Notification:

public class EmailNotification implements NotificationService {
    public void sendNotification(String medium){
        // write Logic using for sending email
    }
}

Mobile Notification:

public class MobileNotification implements NotificationService {
    public void sendNotification(String medium){
        // write Logic using for sending notification via mobile
    }
}

Liskov Substitution Principle

This principle states that “derived classes must be able to substitute for their base classes”. In other words, if class A is a child of class B, then we should be able to replace B with A without interrupting the current behavior of the program.

Consider an example of a square class derived from 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);
    }
}

In the Rectangle class, setting width and height seems perfectly logical. However, in the square class, the SetWidth() and SetHeight() don't make sense because setting one would change the other to match it.

In this case, Square fails the Liskov substitution test 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.

Interface Segregation Principle

This principle applies to Interfaces and it is similar to the single responsibility principle. It states that “ a client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.“.

Let's understand this by the example of a vehicle interface:

public interface Vehicle {
    public void drive();
    public void stop();
    public void refuel();
    public void openDoors();
}

Let's say we now create a Bike class using this Vehicle interface

public class Bike implements Vehicle {
    public void drive() {}
    public void stop() {}
    public void refuel() {}

    // Can not be implemented
    public void openDoors() {}
}

Since Bike doesn't have doors we can't implement the last function.

To fix this, it is recommended to break down the interfaces into small multiple, interfaces so that no class is forced to implement any interface or methods, that it does not need.

public interface Vehicle {
    public void drive();
    public void stop();
    public void refuel();
}
public interface Doors{
    public void openDoors();
}

Creating two classes - Car and Bike

public class Bike implements Vehicle {
    public void drive() {}
    public void stop() {}
    public void refuel() {}
}
public class Car implements Vehicle, Door {
    public void drive() {}
    public void stop() {}
    public void refuel() {}
    public void openDoors() {}
}

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) states that "entities must depend on abstractions (abstract classes and interfaces), and not on concrete implementations (classes). Also, the high-level module must not depend on the low-level module, but both should depend on abstractions".

Suppose there is a book store that enables customers to put their favorite books on a particular shelf.

In order to implement this functionality, we create a Book class and a Shelf class. The Book class will allow users to see reviews of each book they store on the shelves. The Shelf class will let them add a book to their shelf. For example,

public class Book {
    void seeReviews() {}
}


public class Shelf {
     Book book;
     void addBook(Book book) {}
}

Everything looks fine, but as the high-level Shelf class depends on the low-level Book, the above code violates the Dependency Inversion Principle. This becomes clear when the store asks us to enable customers to add their own reviews to the shelves, too. In order to fulfill the demand, we create a new UserReview class:

public class UserReview{
     void seeReviews() {}
}

Now, we should modify the Shelf class so that it can accept User Reviews, too. However, this would clearly break the Open/Closed Principle too.

The solution is to create an abstraction layer for the lower-level classes (Book and UserReview). We’ll do so by introducing the Product interface, both classes will implement it. For example, the below code demonstrates the concept.

public interface Product {
    void seeReviews();
}

public class Book implements Product {
    public void seeReviews() {}
}

public class UserReview implements Product {
    public void seeReviews() {}
}

Now, the Shelf can reference the Product interface instead of its implementations (Book and UserReview). The refactored code also allows us to later introduce new product types (for instance, Magazine) that customers can put on their shelves, too.

public class Shelf {
    Product product;
    void addProduct(Product product) {}
    void customizeShelf() {}
}

Conclusion

In this article, you were presented with the five principles of the SOLID Code. Adhering to SOLID principles can make your project, extendable, easily modifiable, well tested, with fewer complications.

Did you find this article valuable?

Support Apoorv Tyagi by becoming a sponsor. Any amount is appreciated!