SOLID Principle

Sourabhh Sethii
DXSYS
Published in
8 min readMay 12, 2020

SOLID is an acronym for 5 important design principles when doing OOP (Object Oriented Programming) by Robert C. Martin, popularly known as Uncle Bob.

These principles, when combined together, make it easy for a programmer to develop software that are easy to maintain and extend. it seems complicated, but they are pretty simple to grasp. Lets Look into it.

S — Single responsibility principle

O — Open/closed principle

L — Liskov substitution principle

I — Interface segregation principle

D — Dependency inversion principle

S — Single responsibility principle

Do one thing and do it well.Responsibility as a ‘reason to change’, and concludes that a class or module should have one, and only one, reason to be changed.

or

We can say A class should have one and only one reason to change, meaning that a class should have only one job.

Let’s do an example of how to write a piece of code that violates this principle.

Single Responsibility Violation : Here in below example we can see that AddPost method has too much responsibility, AddPost method is added post, as well as loggin error in db and file.

package com.example.SingleResponsibility;import java.util.ArrayList;
import java.util.List;
public class SingleResponsibilityViolation {

void AddPost(List<String> db, String postMessage)
{
try
{
db.add(postMessage);
}
catch (Exception ex)
{
db.add("An error occured: Log error in DB " + ex.toString()); // Log error in DB.
db.add("An error occured: Log error in File " + ex.toString()); // Log error in File.
}
System.out.println(db.get(0));
}

public static void main(String []args){
System.out.println("Hello World");
SingleResponsibilityViolation srv = new SingleResponsibilityViolation();
List<String> arrlist =new ArrayList<String>();
srv.AddPost(arrlist,"String");
}
}

This violates the single responsibility principle. Let’s correct it in below example

By abstracting the functionality that handles the error logging, we no longer violate the single responsibility principle.

Now we have two classes that each has one responsibility; to create a post and to log an error, respectively.

package com.example.SingleResponsibility;import java.util.ArrayList;
import java.util.List;
public class SingleResponsibilityCorrection { private ErrorLogger errorLogger = new ErrorLogger(); void AddPost(List<String> db, String postMessage)
{
try
{
db.add(postMessage);
}
catch (Exception ex)
{
errorLogger.log( db,ex.toString());
}
}

public static void main(String []args){
System.out.println("Hello World");
SingleResponsibilityViolation srv = new SingleResponsibilityViolation();
List<String> arrlist =new ArrayList<String>();
srv.AddPost(arrlist,"String");
}
}
class ErrorLogger
{
void log(List<String> db, String ex)
{
db.add("An error occured: Log error in DB " + ex.toString()); // Log error in DB.
db.add("An error occured: Log error in File " + ex.toString()); // Log error in File.
}
}

The single responsibility principle is one of the most commonly used design principles in object-oriented programming. You can apply it to classes, software components, and microservices.

O — Open(Extension)/closed(Modification) principle

States that software entities (classes, modules, functions, etc.) should be open for extensions, but closed for modification.We can make sure that our code is compliant with the open/closed principle by utilizing inheritance and/or implementing interfaces that enable classes to polymorphic-ally substitute for each other.

Let’s do an example of how to write a piece of code that violates this principle.

package com.exmaple.openclose;import java.util.ArrayList;
import java.util.List;
public class OpenCloseViolation {

void AddPost(List<String> db, String postMessage)
{
if (postMessage.startsWith("#"))
{
db.add(postMessage);
}
else
{
db.add(postMessage);
}

System.out.println(db.toString());
}

public static void main(String []args){
System.out.println("Hello World");
OpenCloseViolation ocv = new OpenCloseViolation();
List<String> arrlist =new ArrayList<String>();
ocv.AddPost(arrlist,"#String");
}
}
/* OutPut
Hello World
[#String] */

In this code snippet we need to do something specific whenever a post starts with the character ‘#’.

In the above implementation violates the open/closed principle in the way this code differs the behavior on the starting letter.

If we later wanted to also include mentions starting with ‘@’, we’d have to modify the class with an extra ‘else if’ in the AddPost()method.

Let’s try to make this code compliant with the open/closed principle by simply using inheritance.

package com.exmaple.openclose;import java.util.ArrayList;
import java.util.List;
public class OpenCloseCorrection {
void AddPost(List<String> db, String postMessage)
{
db.add(postMessage);
System.out.println("OpenCloseCorrection" + db.toString());
}

public static void main(String []args){
System.out.println("Hello World");

LetsExtendOpenClose leoc = new LetsExtendOpenClose();
List<String> arrlist =new ArrayList<String>();
leoc.CreatePost(arrlist,"String LetsExtendOpenClose");

OpenCloseCorrection occ = new OpenCloseCorrection();
occ.AddPost(arrlist,"String OpenCloseCorrection");
}
}
class LetsExtendOpenClose extends OpenCloseCorrection
{
void AddPost(List<String> db, String postMessage)
{
db.add(postMessage);
System.out.println( "LetsExtendOpenClose " + db.toString());
}


}

By using inheritance, it is now much easier to create extended behavior to the OpenCloseCorrection object by overriding the AddPost()method. we can change the code there, without affecting any of these underlying pieces of behavior.

L — Liskov substitution principle

In programming, the Liskov substitution principle states that if A is a subtype of B, then objects of type B may be replaced (or substituted) with objects of type A. As per the LSP, functions that use references to base classes must be able to use objects of the derived class without knowing it. The LSP is popularly explained using the square and rectangle example. Let’s assume we try to establish an ISA relationship between Square and Rectangle. Thus, we call “Square is a Rectangle.”

package com.exmaple.LiskovSubstitution;import java.util.List;/*
* @author Sourabh Sethi
*/
public class LiskovSubstitutionPrinciple {
/**
* In case, we try to establish ISA relationship between Square and Rectangle such that we call "Square is a Rectangle",
* below code would start behaving unexpectedly if an instance of Square is passed
* Assertion error will be thrown in case of check for area and check for breadth, although the program will terminate as
* the assertion error is thrown due to failure of Area check.
*
* @param r Instance of Rectangle
*/
public void calculateArea(Rectangle r) {
r.setBreadth(2);
r.setLength(3);
//
// Assert Area
//
// From the code, the expected behavior is that
// the area of the rectangle is equal to 6
//
assert r.getArea() == 6 : printError("area", r);
//
// Assert Length & Breadth
//
// From the code, the expected behavior is that
// the length should always be equal to 3 and
// the breadth should always be equal to 2
//
assert r.getLength() == 3 : printError("length", r);
assert r.getBreadth() == 2 : printError("breadth", r);
}
private String printError(String errorIdentifer, Rectangle r) {
return "Unexpected value of " + errorIdentifer + " for instance of " + r.getClass().getName();
}
public static void main(String[] args) {
LiskovSubstitutionPrinciple lsp = new LiskovSubstitutionPrinciple();
//
// An instance of Rectangle is passed
//
lsp.calculateArea(new Rectangle());
//
// An instance of Square is passed
//
lsp.calculateArea(new Square());
}
}
/*
* Square class; Square inherits from Rectangle;
* Represents ISA relationship - Rectangle is base class
* @author Sourabh Sethi
*/
class Rectangle {
private int length;
private int breadth;
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getBreadth() {
return breadth;
}
public void setBreadth(int breadth) {
this.breadth = breadth;
}
public int getArea() {
return this.length * this.breadth;
}
}
/*
* Square class; Square inherits from Rectangle;
* Represents ISA relationship - Square is a Rectangle
* @author Sourabh Sethi
*/
class Square extends Rectangle {
@Override
public void setBreadth(int breadth) {
super.setBreadth(breadth);
super.setLength(breadth);
}
@Override
public void setLength(int length) {
super.setLength(length);
super.setBreadth(length);
}
}

This is but one simple example of how to correct a violation of this principle.

We try to establish ISA relationship between Square and Rectangle such that we call “Square is a Rectangle”, Code would start behaving unexpectedly if an instance of Square is passed Assertion error will be thrown in case of check for area and check for breadth, although the program will terminate as the assertion error is thrown due to failure of Area check.

I — Interface segregation principle

In programming, the interface segregation principle states that no client should be forced to depend on methods it does not use.if you’re used to using interfaces, chances are that you’re already applying this principle.

Do not add additional functionality to an existing interface by adding new methods.Instead, create a new interface and let your class implement multiple interfaces if needed.

Let’s look at an example of how to violate the interface segregation principle.

package com.example.violation;public interface Number {
void add();
}
interface NumberAddRemove
{
void add();
void remove();
}

In above example, let’s pretend that I first have an NumberAddRemove interface with the signature of a add()method.

Later on, I modify this interface by adding a new method remove(), so it becomes like the NumberAddRemove interface.

Let’s try to make this code compliant with Interface segregation principle.

package com.example.correction;
interface NumberAdd
{
void add();
}
interface NumberRemove
{
void remove();
}

This is where we violate the interface segregation principle. Instead, simply create a new interface. If any class might need both the add()method and the remove()method, it will implement both interfaces.

D — Dependency inversion principle

The dependency inversion principle is a way to decouple software modules. This principle states that 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 look at an example.

package com.example.voilation;
import java.util.*;
public class ListUpdateViolationDependencyInversionPrinciple {
static ListAdd listAdd = new ListAdd();
static ListDelete listDelete = new ListDelete();

public static void main(String []args){
System.out.println("Hello World");

List<Integer> arrlist = new ArrayList<Integer>();
listAdd.add(arrlist,1);
listAdd.add(arrlist,2);
listDelete.delete(arrlist,1);
}
}class ListAdd {

void add(List<Integer> list , int number ) {
list.add(number);
System.out.println("add to list" + list.toString());

}
}class ListDelete {

void delete(List<Integer> list , int number ) {
for(int i = 0; i < list.size(); i++) {
if (list.get(i) == number) {
list.remove(i);
}
}
System.out.println("delete " + number + " from list" + list.toString());
}
}

Observe how we create the ListAdd, ListDelete instance from within the ListUpdateViolationDependencyInversionPrinciple class.

This is a violation of the dependency inversion principle.

If we wanted to use a different kind of ListAdd, ListDelete , we would have to modify the ListUpdateViolationDependencyInversionPrinciple class.

Let’s fix this by using dependency injection.

Let’s look into design pattern known as a dependency inversion pattern, implemented by using dependency injection. Typically, dependency injection is used simply by ‘injecting’ any dependencies of a class through the class’ constructor as an input parameter.

package com.example.correction;import java.util.ArrayList;
import java.util.List;
public class ListUpdateCorrectionDependencyInversionPrinciple { private ListAdd addlist;
private ListDelete deleteList;
public ListUpdateCorrectionDependencyInversionPrinciple(){
} public ListUpdateCorrectionDependencyInversionPrinciple(ListAdd addlist,
ListDelete deleteList){
this.addlist = addlist;
this.deleteList = deleteList;
} void LetsMakeACall() {
List<Integer> arrlist = new ArrayList<Integer>();
arrlist.add(1);
addlist.add(arrlist,1);
addlist.add(arrlist,2);
deleteList.delete(arrlist,1);
}
}class ListAdd { void add(List<Integer> list , int number ) {
list.add(number);
System.out.println("add to list" + list.toString());
}}class ListDelete { void delete(List<Integer> list , int number ) {
for(int i = 0; i < list.size(); i++) {
if (list.get(i) == number) {
list.remove(i);
}
}
System.out.println("delete " + number + " from list" + list.toString());
}
}

By using dependency injection we no longer rely on the ListUpdateCorrectionDependencyInversionPrinciple class to define the specific type of logger.

Conclusion:

Applying these principle will help us to develop a reusable, maintainable, scalable and easy testable codebase. If you want to make SOLID application using SOLID Principles.

Code Repo : https://github.com/Sourabhhsethii/SOLID

Refernces :

http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod

https://www.researchgate.net/publication/323935872_SOLID_Python_SOLID_principles_applied_to_a_dynamic_programming_language

--

--

Sourabhh Sethii
DXSYS
Editor for

I am an author of Building Digital Experience Platform and I am passionate about emerging technologies. https://sourabhhsethii.com/