In object oriented programming (OOP), SOLID is a mnemonic acronym for below five design principles intended to make software designs more understandable, flexible and maintainable.

  • Single Responsibility Principle
  • Open/Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

Single Responsibility Principle

This principle states that a class should have only one reason to change. Classes should be designed, such that one class at the most is responsible for doing one task or functionality among the whole set of responsibilities that it has. Classes defined using the Single Responsibility Principle are inherently cohesive in nature, i.e. class attributes and behavior are specific to that single functionality only to which the class caters to.

IEmail interface and Email class have 2 responsibilities. It deals with sending and receiving emails. It also deals with Content field. Even if content is a string maybe we want in the future to support HTML or other formats. If we keep only one class, each change for a responsibility might affect the other one.

// IEmail
public interface IEmail {
  public void setSender(String sender);
  public void setReceiver(String receiver);
  public void setContent(String content);
}

class Email implements IEmail {
  // Set sender
  public void setSender(String sender) {

  }

  // Set receiver
  public void setReceiver(String receiver) {

  }

  // Set content
  public void setContent(String content) {
  }
}

Creating a new interface and class called IContent and Content to split the responsibilities will make the design more robust. Having only one responsibility for each class give us a more flexible design.

// Single responsibility principle
interface IEmail {
  public void setSender(String sender);
  public void setReceiver(String receiver);
  public void setContent(IContent content);
}

interface Content {
  public String getAsString();
}

class Email implements IEmail {
  // Set sender
  public void setSender(String sender) {
  
  }

  // Set receiver
  public void setReceiver(String receiver) {
  
  }

  // Set content
  public void setContent(IContent content) {
  
  }
}

Open Close Principle

Open Close Principle states that design of code should be done in a way that new functionality should be added with minimum changes in the existing code. Software entities like classes, modules and functions should be open for extension but closed for modifications. A module will be said to be closed if it is available for use by other modules. If a class is re-usable or specifically available for extending as a base class then it is open.

In below example, AreaCalculator class method calculateRectangleArea(), which finds area of Rectangle. Now for new shape Circle, AreaCalculator class was modified to add circle calculations through a new method calculateCircleArea().

public class Rectangle{
 public double length;
 public double width;
}

public class Circle{
 public double radius; 
}

public class AreaCalculator{
  public double calculateRectangleArea(Rectangle rectangle){
    return rectangle.length *rectangle.width;
  }

  public double calculateCircleArea(Circle circle){
    return (22/7)*circle.radius*circle.radius;
  } 
}

As the types of shapes grows AreaCalculator keeps on changing and any consumers of this class will have to keep on updating their libraries which contain AreaCalculator. Below example modifies above design to comply with Open/Closed Principle. It makes design extensible by defining a base type Shape and have Circle and Rectangle implement Shape.

public interface Shape{
  public double calculateArea();
}
 
public class Rectangle implements Shape{
  double length;
  double width;
  public double calculateArea(){
    return length * width;
  }
}
 
public class Circle implements Shape{
  public double radius;
  public double calculateArea(){
    return (22/7)*radius*radius;
  }
}

public class AreaCalculator{
  public double calculateShapeArea(Shape shape){
    return shape.calculateArea();
  }
}

AreaCalculator class now fully removes design flaws noted above and gives a clean solution which adheres to the Open-Closed Principle. Design is now correct as per Open Closed Principle due to the following reasons

  • Design is open for extension as more shapes can be added without modifying the existing code.
  • This design is also closed for modification. AreaCalculator class is complete with respect to area calculations.

Liskov Substitution Principle

This principle defines that objects of a super class shall be replaceable with objects of its sub classes without breaking the application. That requires the objects of sub classes to behave in the same way as the objects of super class. Derived types must be completely substitute for their base types. It ensures that inheritance is used correctly.

All circles are inherently ellipses with their major and minor axes being equal. In terms of classes, Circle class extend Ellipse class. In this case an object of type Circle can be assigned to a reference of type Ellipse. So, all the methods in Ellipse can be invoked on this object of Circle which is stored in it. setLengthOfAxisX() and setLengthOfAxisY() method defined in Eclipse class set the length of X and Y of an ellipse. Calling any of these two methods on an object of type would lead to a circle no longer being a circle, as in a circle length of major and minor axes have to be equal. This is a violation scenario of Liskov Substitution Principle as assigning a sub type object to a super type reference doesn’t work which is not in line with the principle.

Interface Segregation Principle

This principle states that clients should not be forced to implement interfaces they don’t use. Instead of big interface, many small interfaces should be preferred based on groups of methods, each serving one sub-module. Interface Segregation Principle advocates that instead of having a single interface catering to all the clients, it is better to have multiple interfaces with each interface containing methods for a client-specific functionality.

Assume a Manager class which represent the person which manages the workers. And there are two types of workers i.e. some average and some very efficient workers. Both types of workers works and they need a daily launch break to eat. But now some robots came in the company and they work as well, but they don’t eat so they don’t need a launch break.

The new Robot class is forced to implement the eat method of Worker interface, and can have undesired effects in the application. According to the Interface Segregation Principle, a flexible design will not have polluted interfaces. Following is the code supporting the Interface Segregation Principle. By splitting the Worker interface in 2 different interfaces the new Robot class is no longer forced to implement the eat method.

// Interface segregation principle example
interface IWorkable {
  public void work();
}

interface IFeedable{
  public void eat();
}

class Worker implements IWorkable, IFeedable{
  public void work() {
    // ....working
  }

  public void eat() {
    //.... eating in launch break
  }
}

class Robot implements IWorkable{
  public void work() {
    // ....working
  }
}

class SuperWorker implements IWorkable, IFeedable{
  public void work() {
    //.... working much more
  }

  public void eat() {
    //.... eating in launch break
  }
}

class Manager {
  Worker worker;

  public void setWorker(Worker w) {
    worker=w;
  }

  public void manage() {
    worker.work();
  }
}

Dependency Inversion Principle

This principle talks about the coupling between the different classes or modules. It focuses on the approach where the higher classes are not dependent on the lower classes instead depend upon the abstraction of the lower classes. It consists of two parts:

  • 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.
// A BackEnd Developer
public class BackEndDeveloper {
    public void writePHP() {
    }
}

// FrontEnd developer:
public class FrontEndDeveloper {
    public void writeCSS() {
    }
}

// Project class
public class Project {
    private BackEndDeveloper 	backDev  = new BackEndDeveloper();
    private FrontEndDeveloper 	frontDev = new FrontEndDeveloper();

    public void implement() {
        backDev.writePHP();
        frontDev.writeCSS();
    }
}

Project class is a high-level module, and it depends on low-level modules such as BackEndDeveloper and FrontEndDeveloper. It violate the first part of the dependency inversion principle. writePHP() and writeCSS() are bound to the corresponding classes and those are used in project scope. Thus, the second part of the dependency inversion principle is violated. Above design issues can be resolved by implementing an Developer interface.

// Developer interface
public interface Developer {
  void develop();
}

public class BackEndDeveloper implements Developer {

  @Override
  public void develop() {
    writePHP();
  }

  private void writePHP() {

  }
}

public class FrontEndDeveloper implements Developer {

  @Override
  public void develop() {
    writeCSS();
  }

  public void writeCSS() {

  }
}

In order to tackle the violation of the first part, modify Project class so that it will not depend on the FrontEndDeveloper and the BackendDeveloper classes. Now low-level modules and their details depend on abstractions.

public class Project {
    private List<Developer> developers;
    public Project(List<Developer> developers) {
        this.developers = developers;
    }

    public void implement() {
        developers.forEach(d->d.develop());
    }
}

Principle of Least Knowledge

The Law of Demeter or Principle of Least Knowledge provides guidelines for designing a system with minimal dependencies. A client should only have knowledge of an objects members, and not have access to properties and methods of other objects via the members. In other words any method of an object should call only methods belonging

  1. Methods of itself
  2. Methods of Object passed as an argument
  3. Method of object, which is held in instance variable i.e. component objects
  4. Any Object which is created locally
// Principle of least knowledge example
public class LawOfDelimter {

  public void process(Order order) {

    // As per rule 1, this method invocation is fine, because order is a argument of process() method
    Message msg = order.getMessage();

    // This method call is a violation, as we are using msg, which we got from Order.
    msg.normalize();

    // This is also a violation, instead using temporary variable it uses method chain.
    order.getMessage().normalize();

    // This is OK, a constructor call, not a method call.
    Instrument sym = new Instrument();

    // As per rule 4, this method call is OK, because instance of Instrument is created locally.
    sym.populate(); 
  }
}

Hollywood Principle

This principle is closely related to the Dependency Inversion Principle. The intent is to take care of structure and implement dependencies wisely. This principle defends against dependency rot. Dependency rot happens when each component depends upon every other component i.e. dependency happens in each direction (Up, sideways, downward). Hollywood Principle restricts us to make dependency in only one direction.

Hollywood Principle specifies how higher level component and lower level component interact without creating dependencies. Say a company has a resume upload portal where job seekers upload their resumes. When the company does some on-campus recruiting, they will call them by sending an email to their inbox. We can implement the same thing via the Hollywood principle. Job seekers upload their resumes in the job portal, and Company sends mail to them when an on-campus event occurs.

public class Resume {
    private String email;
    private String name;
}

public class JobPortal {

    private static JobPortal portal = new JobPortal();

    private List<Resume> resumeList = new ArrayList<Resume>();

    public static JobPortal get(){

        return portal;
    }

    public void uploadContent(String mail ,String name)
    {
        Resume resume = new Resume();

        resume.setName(name);
        resume.setEmail(mail);
    }

    public void triggerCampusing(){

        for(Resume resume : resumeList){
            System.out.println("Sending mail to " + resume.getName() + " at " + resume.getEmail());
        }
    }
}

public class HollywoodTest {

    public static void main(String[] args) {

        JobPortal.get().uploadContent("abc@xyz.com", "abc");
        JobPortal.get().uploadContent("xyz@123.com", "xyz");   

        // Now trigger campusing
        JobPortal.get().triggerCampusing();
    }
}

Reference

SOLID