SOLID: The base principles of Object Oriented Design

SOLID: The base principles of Object Oriented Design

SOLID is a mnemonic acronym that helps define the five basic object-oriented design principles:

  • Single Responsibility Principle
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle
The Single Responsibility Principle A class should have one, and only one, reason to change.
The Open Closed Principle You should be able to extend a classes behavior, without modifying it.
The Liskov Substitution Principle Derived classes must be substitutable for their base classes.
The Interface Segregation Principle Make fine grained interfaces that are client specific.
The Dependency Inversion Principle Depend on abstractions, not on concretions.

Single Responsibility Principle

The Single Responsibility Principle states that: A class should have one, and only one, reason to change.

The idea behind this principle is to design a class that has one responsibility or various methods with unique functionality. According to this principle, a method should not do more than one task at a time. Each function must be designated a unique task.

How many responsibilities?

class Employee {
    public Pay calculatePay() {...}
    public void save() {...}
    public String describeEmployee() {...}
}

The correct answer is three.

Here we have pay 1) calculation logic with 2) database logic and 3) 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-Closed Principle

The Open-Closed Principle states that: Software entities (classes, modules, functions, etc) should be open for extension, but closed for modification.

This principle basically states that we have to design our modules, classes, and functions in a way that when a new functionality is needed, we should not modify our existing code but rather write new code that will be used by existing code.

Let us assume we are trying to calculate the area of some shapes. So let's take the example of a rectangle and a circle. The classes for these have been formed in the following code:

public class Rectangle {
    private double length;
    private double height;
    // getters/setters ...
}

public class Circle {
    private double radius;
    // getters/setters ...
}

So a common function used to calculate the area of both the rectangle and the circle would look something like this:

public class AreaManager {
    public double calculateArea(ArrayList<Object>... shapes) {
        double area = 0;
        for (Object shape : shapes) {
            if (shape instanceof Rectangle) {
                Rectangle rect = (Rectangle)shape;
                area += (rect.getLength() * rect.getHeight());
            } else if (shape instanceof Circle) {
                Circle circle = (Circle)shape;
                area += (circle.getRadius() * cirlce.getRadius() * Math.PI;
            } else {
                throw new RuntimeException("Shape not supported");
            }
        }
        return area;
    }
}

As can be seen from the preceding function, as new shapes are introduced, the calculateArea function will grow bigger and lots of handling and changes will be required. This violates the Open/Closed Principle. A way to resolve this is by using a common interface:

public interface Shape {
    double getArea();
}

Both the rectangle and circle can implement this interface by which the method to calculate the area will remain inside the object class instead of the AreaManager.

So now the rectangle and circle classes will look something like this:

public class Rectangle implements Shape {
    private double length;
    private double height;
    // getters/setters ...
    @Override
    public double getArea() {
        return (length * height);
    }
}

public class Circle implements Shape {
    private double radius;
    // getters/setters ...
    @Override
    public double getArea() {
        return (radius * radius * Math.PI);
    }
}

Now, as the methods for calculating the areas are present inside the objects, the AreaManager will look something like this:

public class AreaManager {
    public double calculateArea(ArrayList<Shape> shapes) {
        double area = 0;
        for (Shape shape : shapes) {
            area += shape.getArea();
        }
        return area;
    }
}

Now we can calculate the total area without ever needing to change the calculateArea method. The same shape interface can be now used in new classes to calculate the area without changing the AreaManager.

Liskov Substitution Principle

The Liskov Substitution Principle states that: Child classes should never break the parent class' type definitions.

According to this principle, a subclass should override the parent class's methods in a way that does not break functionality from a client's point of view.

According to this principle, if a class is extending another class, the functionality of the child class should not conflict with that of its parent.

We can demonstrate this with the following example:

public class Rectangle {
    private double length;
    private double height;
    public void setLength(double length) {
        this.length = length;
    }
    public void setHeight(double height) {
        this.height = height;
    }
    public double getLength() {
        return length;
    }
    @Override
    public double getHeight() {
        return height;
    }
    public double getArea() {
        return (length * height);
    }
}

Here we have a rectangle. As we know, a square is also a type of rectangle, so it can extend the Rectangle class. Also we know that the height and the width of the square have to be the same so the getter can be written like this:

public class Square extends Rectangle {
    @Override
    public void setHeight(double height) {
        this.length = height;
    }

    @Override
    public void setLength(double length) {
        this.length = length;
        this.height = length;
    }
}

As can be seen from the preceding definition, we can get a rectangle also from the square implementation.

So now let's get an instance of Rectangle from the Square class:

Rectangle r = new Square();
r.setHeight(5);
r.setLength(10);

Now if we try to get the area, we will get 100 instead of 50, as a square has both the same length and height, which is not the case with a rectangle, and this violates the Liskov Substitution Principle.

A simple example of the Liskov Substitution Principle would be a List and ArrayList. An ArrayList implements a List but it does not change the basic functionality of the List.

Interface Segregation Principle

The Interface Segregation Principle states that: No client should be forced to depend on methods it does not use.

According to this principle, if an interface has too many methods, then we need to divide the interface into smaller interfaces with fewer methods. A simple example of this principle is shown next.

Let us assume we are using a custom interface to detect various states of a view:

public interface ClickListener {
    public void onItemClickListener(View v, int pos);
    public void onItemLongClickListener(View v, int pos);
    public void onItemPressListener(View v, int pos);
    public void onSelectedListener(View v, int pos);
}

Now, while implementing this listener, we only want the onItemClickListener or the onItemLongClickListener the others are not required but we still have to use them in the code. This violates the Interface Segregation Principle.

Now we can easily resolve this by splitting the interface into smaller interfaces, like this:

public interface ClickListener {
    public void onItemClickListener(View v, int pos);
    public void onItemLongClickListener(View v, int pos);
}

public interface HoldListener {
    public void onItemPressListener(View v, int pos);
    public void onSelectedListener(View v, int pos);
}

Now we will only initialize the ClickListener and use its methods instead of the old interface where we had to utilize four methods. Here we have segregated them into two different interfaces.

Dependency Inversion Principle

The Dependency Inversion Principle states that:

1. High-level modules should not depend on low-level modules. Both should depend on abstractions. 2. Abstractions should not depend upon details. Details should depend upon abstractions.

The best way to explain this principle is by giving an example. Let's assume we have a Worker class that is a low level class and a Manager class that is a high level class. The Manager class contains many complex functionalities which it implements along with the Worker class, so the classes will look something like this:

class Worker {
    public void work() {
        // working
    }
}

class Manager {
    // other functionality
    Worker worker;
    public void setWorker(Worker w) {
        worker = w;
    }
    public void manage() {
        worker.work();
    }
}

Here the Manager class has been implemented and is directly associated with the Worker class due to which changes in the Worker class directly affect theManager`` class.

If we have to add another class which would be a parent of the Worker class, and the Worker class does similar work to that of the Manager class, it will require lots of changes.

To make it easier to add the Manager class, we will use interfaces:

interface IWorker {
    public void work();
}

class Worker implements IWorker {
    public void work() {
    // working
    }
}

class SuperWorker implements IWorker{
    public void work() {
    // working much more
    }
}

class Manager {
    IWorker worker;
    public void setWorker(IWorker w) {
        worker = w;
    }
    public void manage() {
        worker.work();
    }
}

Now the both the Worker and SuperWorker class implement the IWorker, while the Manager class directly uses the IWorker to complete its functionality by which changes to the Worker and SuperWorker do not affect the Manager class.

comments powered by Disqus