S.O.L.I.D Principles in Object-Oriented Designing

Thisuri Bandaranayake
Analytics Vidhya
Published in
8 min readMay 31, 2020
Image by
Hitesh Choudhary at Unsplash

S.O.L.I.D is an acronym that defines the first five principles of object-oriented designing. This was introduced by Robert C. Martin. These five principles define how to design a software solution that matches the coding standards and how to avoid bad design practices. Let’s further look into these principles with simple examples in JAVA.

What are the S.O.L.I.D principles?

  • Single-responsibility principle
  • Open-closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

Why we use S.O.L.I.D principles?

S.O.L.I.D principles make it easy for programmers to design very flexible solutions where flexibility is very important in programming. With the use of S.O.L.I.D in your solution, the code becomes easier to understand, extendable, easy to debug and refactor. Bad practices in designing may lead to extremely bad results such as the solution becoming inflexible and brittle and a small change may lead to many bugs which will totally fail the system. To overcome all those bad practices and to develop a system that matches the industrial standards let’s practice S.O.L.I.D principles.

S.O.L.I.D in detail…

01)Single-responsibility principle

“A class should have one and only one reason to change”.

This means that a class should have only one purpose or responsibility and all the methods and attributes in that class should relate directly only to that purpose or the responsibility. Simply, all the methods and variables in a class should have the same goal. If the purpose of a method or an attribute is not as same as the purpose of the class, then it should put into a separate class. Let’s understand it more clearly with the below example.

An example where the single-responsibility principle is violated.

Look at the above example. At first look, it seems fine. Of course, it works fine and gives the desired outputs. But it violates the single responsibility principle. The purpose of the TextMaker class is to manipulate texts such as build new texts, append texts, modify texts etc. But in this class, rather than manipulating texts, texts are printed in different formats. Well, printing the texts in different formats is not a responsibility of the TextMaker. Here TextMaker has done the duties of a TextPrinter where it has violated the first principle.

Let’s see how to correct that in order to ensure the single-responsibility principle. Let’s give the responsibility of printing the text in different formats to a new class called TextPrinter. So that the TextPrinter will print the texts while the TextMaker will manipulate texts. Now the responsibilities are separated and given to relevant classes.

An example where the single-responsibility principle is ensured.

Now the two classed have their own responsibilities and ensures the single-responsibility principle. A very important point to be remembered in this principle is, we should first understand the purpose of the class very clearly.

02)Open-closed principle

“Software entities (classes, modules, functions, etc) should be open for extensions, but closed for modifications”

This principle says that an entity should be able to extend easily without changing its content. It promotes you to write codes in a way that you can add new functionalities to them without changing the existing code. This principle prevents situations where changes to one class effect in changing all other depending classes. Bertrand Mayer proposed inheritance as a solution to this problem. But as inheritance introduces tight coupling Robert C. Martin and others introduced polymorphism as the solution. There we use interfaces which are always closed to modifications but are open to extensions. Let’s see an example of this principle.

An example where the open-closed principle is violated.

Look a the above example, at the first look, it seems ok. Yes, it works fine and gives the correct output. But this code has violated the open-closed principle. Look at the method area(). The area function only calculates the area of the circle and rectangle. Think if we want to add a new shape like a Square or a Triangle. Then we have to modify the area function in the superclass in order to calculate the area of the new shape. Each time a new shape is added, we have to modify the superclass area method. This is a bad practice. The entities in this example are open to modifications. Let’s change the code in order to ensure the open-closed principle where the entities are open only for extensions and closed for modifications.

An example where the open-closed principle is ensured.

Look at the above example. I have used interfaces instead of inheritance in order to ensure loose coupling. Here the interface Shapes has an abstract method area(). All other classes of shapes are implementations of the Shape interface and override the area method with their own implementations. This way, no matter how many new classes are added, they override the area method without modifying any other entity. In this new code, the entities are closed for modifications but open for extensions. Now we have ensured the open-closed principle.

03) Liskov substitution principle

“Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.”

In simple words, the subclasses should be substitutable for their parent class. When considering interfaces(abstraction), any implementation of an interface should be substitutable for the places where the interface is accepted. If said in common, subtypes should be substitutable for their supertype references without affecting the program execution.

Let’s see an example where the above principle is violated.

An example where the Liskov substitution principle is violated.

In the above code, Bird is the superclass and Parrot and Penguine are subclasses. The following class Tester is written to run and test the above code.

See the above example code. When we run the flyBird() method in Tester class, in the Bird object list object x and y will run the fly() method fine. But when z.fly() runs, an exception will occur. That is because the Bird class was not substitutable by Penguin class where the Liskov substitution principle has been violated. Let’s see how to correct this in order to ensure the Liskov substitution principle.

By factoring the common features into the same classes we can correct the above code to ensure the Liskov substitution principle. Let’s separate the flying birds and non-flying birds as follows.

An example where the Liskov substitution principle is ensured.

Now all the superclasses are substitutable by the subclasses where the code ensures the Liskov substitution principle.

The Liskov substitution principle(LSP) is very closely related to the open-closed principle. Whenever a code violates the Liskov-substitution principle it also violates the open-closed principle. That happens like this, when the subtype is not replaceable by the supertype, then we have to do changes in the current solution in order to support the supertype which is a violation of the open-closed principle.

04) Interface segregation principle

“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.”

This principle says that no client should be forced to depend on methods it doesn’t use. When developing a software solution, the users frequently request for new features. When fulfilling those requests it’s pretty easy to violate this principle. The aim of this principle is to minimize the side effects of changes done according to user requests. For this, the solution is split into multiple independent parts.

Let’s understand the way this principle is violated using the following example.

An example where the interface segregation principle is violated.

At the first look, this code works fine. But when observed well it is not in the correct standard. In the above code, the SportsmanInterface has four methods. A cricketer is a subtype of a sportsman. By implementing the SportsmanInterface by the class Cricketer it has to implement the methods like longJump() and hurdles() that are never used by the Cricketer. Here the class Cricketer is forced to depend on two methods that it will never use. There, the Interface segregation principle is violated. Let’s see how to correct the above example code by ensuring the Interface segregation principle.

An example where the interface segregation principle is ensured.

Now the SportsmanInterface has only the method compete() which is common to all the sportsman. A new interface named CricketerInterface with the methods common to all the cricketers is added. Now we can create a Cricketer class as an implementation of CricketerInterface and there, it has to implement only the needed methods in it. Now it is not forced to implement useless methods in it. This way the Interface segregation principle is ensured.

05) Dependency inversion principle

“Entities must depend on abstractions, not on concretions. ”

High-level modules provide the complex logics and the low-level modules provide utility features. These two modules should be loosely coupled so that the changes in the low-level modules doesn’t affect the high-level modules. This way the Dependency Inversion principle promotes loose coupling. In other words, this principle can be described as a specific form of decoupling the modules.

This principle consists of two parts as follows
1. High-level modules should not depend on low-level modules. Both should depend on abstractions
2. Abstractions should not depend on details. Details should depend on abstractions.

Let’s see how the above principle is violated with an example.

An example where the dependency inversion principle is violated.

In the above example, CricketTeam is the high-level module and Batsman and Bowler are low-level modules. But here, the class CricketTeam depends on the two low-level modules Batsman and Bowler violating the first part of the Dependency inversion principle.

Look at the implementation of the class CricketMatch, the methods doBatting() and doBowling() are methods of the Batsman and Bowler classes. They are the forms of playing which mean they are details. Here the details do not depend on the abstraction. That way the second part of the principle is violated.

Let’s see how to correct the above code to ensure the Dependency inversion principle.

An example where the dependency inversion principle is ensured.

To address the above first problem I have introduced an abstraction (interface) called CricketPlayer with the abstract method play(). Now the classes Batsman and Bowler are implementations of the CricketPlayer abstraction. Now the class CricketMatch which is the High_level module doesn't depend on the low-level modules but depends only on the abstraction. Also, the low-level modules and their details depend on the abstraction ensuring the Dependency inversion principle.

By applying S.O.L.I.D principles in your design you will write codes in the proper standards and you will find them extremely useful. By ensuring these principles your code will easily be extended, modified, tested and refactored without facing difficulties.

Hope this article would help you to get some idea about S.O.L.I.D principles.

Enjoy reading!!!!!!!!

--

--