SOLID principles are a set of design guidelines that helps us write better code and follow best practices while programming.

  • S → Single responsibility
  • O → Open / Closed
  • L → Liskov substitution
  • I → Interface segregation
  • D → Dependency inversion

S → Single Responsibility

Each class should have only one sole responsibility and not be filled with excessive functionality.

For example, let’s assume there’s a Square class, a Circle class with their own private member variables. The Square class contains the length of a side of the square. In case of Circle class, it’s the radius of the circle. There’s also an AreaCalculator class which has a sum method which checks to see if the instance of the shape passed is a Circle or Square and then consequently calculates the area of the shape. The main method has an instance of AreaCalculator class and it adds together the sum of all shapes.

Now, if we modify the AreaCalculator class to also contain various methods to print the sum of the shapes in various formats, that would violate the Single Responsibility principle.

The correct way to comply with Single Responsibility would be to create a separate class called AreaPrinter which will take care of printing the area in all formats.

O → Open / Closed

Classes should be open for extension, but closed for modification. In other words, existing classes should not have to be modified or rewritten in order to implement new functionalities.

If a new shape (say Rectangle) was to be added, then the AreaCalculator class would need to be modified and the functionality to check and calculate area for a rectangle would be added. But this would be a violation of Open / Closed principle.

The correct way would be to add an interface called Shape with the a method declaration to calculateArea(). All other shapes, Rectangle, Square, and Circle would implement Shape and have their own implementation for area calculation. Finally the new class, Rectangle will also implement Shape interface and have its own implementation for area calculation.

The AreaCalculator class would simply accept a (generic) shape and call it’s area calculation method as specified by the Shape interface.

L → Liskov Substitution

Every subclass or derived class should be substitutable with their base or parent class.

Continuing with the last scenario, there’s a new class called NoShape which implements the Shape interface. The NoShape class however throws an IllegalStateException in its area calculation method.

So in the main method if we try to instantiate an object of NoShape class like:

Shape noShape = new NoShape();

and run the program, then we get an IllegalStateException in the output. So the NoShape violates the Liskov substitution principle.

However, with other shapes, such as Rectangle, Circle and Square, Liskov substitution principle remains valid.

Shape square = new Square();
Shape circle = new Circle();
Shape rectangle = new Rectangle();

I → Interface Segregation

Interfaces should not force classes to implement what they can’t do. Large interfaces should be divided into small ones.

A cube is a 3D shape. Unlike 2D shapes like squares, rectangles and circles, cubes also have a volume.

Let’s add a new calculateVolume() method declaration to the Shape interface. Then we can calculate volume for the Cube class too. But all other existing shapes such as Rectangle, Square and Circle will now have errors because they can’t implement the calculateVolume() method. And that would violate the interface segregation principle.

To comply with interface segregation principle, a new interface called ThreeDimensionalShape should be created and that interface should have the calculateVolume() method. Since Cube has both area and volume, it will need to implement both Shape and ThreeDimensionalShape interfaces containing both calculateArea() and calculateVolume() method implementations. All other shapes - Circle, Rectangle and Square will implement only the Shape interface containing only the calculateArea() method implementation.

D → Dependency Inversion

Components should depend on abstractions, not on concretions. In other words, dependent components should depend on interfaces, not on concrete implementations.

The AreaCalculator class has a sum() method which accepts shapes and calculates the sum of their areas. The AreaPrinter class depends on a concrete implementation of AreaCalculator class because it uses its sum() method to get the sum of areas, which it then prints to console. This is a violation of the Dependency Inversion principle.

The correct way should be:

  • AreaPrinter class should depend on an area calculator interface that declares a sum() method.
  • The AreaCalculator class will implement the mentioned interface and implement its sum() method.
  • The AreaPrinter class’s constructor will set the private interface member variable to the constructor argument variable which is the interface itself.
  • The main class can then use AreaPrinter class by passing an implementation of the area calculator interface. If there are multiple implementations of the area calculator interface, all of them can be used simultaneously in different AreaPrinter instances as the AreaPrinter class constructor is compatible with any implementation of the area calculator interface.

References