SOLID Principles

Ishan Aggarwal
8 min readFeb 5, 2023

--

S — Single Responsibility Principle

O — Open Close Design Principle

L — Liskov Substitution Principle

I — Interface Segregation Principle

D — Dependency Inversion Principle

Advantages

  • Helps to write clean clone (more readable and extendable)
  • Easy to test the code blocks
  • Avoid writing duplicate code (and more re-usable code)
  • Easy to maintain the code
  • Debugging time reduces significantly.
  • Easy to do knowledge sharing with peers

Single Responsibility Principle

A class should not have more than one reason to change. If there are multiple responsibilities help by a single class — it may lead to introducing bugs in existing piece of code and breaking one functionality while making changes in another functionality.

version (v0)

package solid.v0;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Product {
private String name;
private int price;
}

package solid.v0;

import lombok.Data;
@Data
public class Invoice {
private Product product;
private int quantity;
private int totalPrice;
public Invoice(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
private int totalPrice() {
this.totalPrice = this.product.getPrice() * this.quantity;
return this.totalPrice;
}
private void printInvoice() {
System.out.println("Total price is : " + this.totalPrice);
}
private void saveInvoice() {
// save invoice to db
}
}
// Now there are multiple reasons to change this class:
// 1. If there is change in the way we calculate the total price, then we need to update this class
// 2. If there is change in the way we print the invoice, then we need to update this class
// 3. If there is change in the way we save the invoice to db, then we need to update this class
// It does not follow the Single Responsibility Principle. It is doing multiple things.
// So, we need to refactor this class to follow the Single Responsibility Principle.
// We can do this by creating a new class called InvoicePrinter and move the printInvoice() method to that class.
// We can do this by creating a new class called InvoiceSaver and move the saveInvoice() method to that class.
// Now, this class will only be responsible for calculating the total price.
// We can also create a new class called InvoiceCalculator and move the totalPrice() method as well to that class.

Open Closed Design Principle

We should not make changes/ modifications in existing classes to provide new functionality. Instead we should extend the existing classes to support add-on features/ functionalities.

O stands for Open for extension and closed for Modifications.

version (v1)

package solid.so.v1;

import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class Product {
private String name;
private int price;
}
package solid.so.v1;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class Invoice {
private Product product;
private int quantity;
private int totalPrice;
public Invoice(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
public int totalPrice() {
this.totalPrice = this.product.getPrice() * this.quantity;
return this.totalPrice;
}
}
package solid.so.v1;
public class InvoicePrinter {
private Invoice invoice;
public InvoicePrinter(Invoice invoice) {
this.invoice = invoice;
}
public void printInvoice() {
System.out.println("Total price is : " + this.invoice.totalPrice());
}
}
package solid.so.v1;
public class InvoiceSaver {
private Invoice invoice;
public InvoiceSaver(Invoice invoice) {
this.invoice = invoice;
}
public void saveInvoice() {
System.out.println("Saving invoice to DB: " + this.invoice.totalPrice());
}
}
// Now in this class we are saving invoice to Database.
// Let's say in future requirement comes to save the invoice to file.
// We need to change the code in this class. This is not good. It violates the Open Close Principle.
// We should not make any changes in this class. We should create a new class for saving invoice to file.
// So, we will create an interface called InvoiceSaver and move the saveInvoice() method to that interface.
// Then both DBInvoiceSaver and FileInvoiceSaver will implement this interface.
// and provide their own implementation of saveInvoice() method.
// This way we will be able to save invoice to DB and file without changing the code in this class.
// and it will help us to follow the Open Close Principle and code remains extensible.

Extensible Version (v2)

package solid.so.v2;

public interface InvoiceSaver {
public void saveInvoice();
}
package solid.so.v2;
public class DBInvoiceSaver implements InvoiceSaver {
private Invoice invoice;
public DBInvoiceSaver(Invoice invoice) {
this.invoice = invoice;
}
@Override
public void saveInvoice() {
System.out.println("Saving invoice to DB: " + this.invoice.totalPrice());
}
}
package solid.so.v2;
public class FileInvoiceSaver implements InvoiceSaver {
private Invoice invoice;
public FileInvoiceSaver(Invoice invoice) {
this.invoice = invoice;
}
@Override
public void saveInvoice() {
System.out.println("Saving invoice to file: " + this.invoice.totalPrice());
}
}
package solid.so.v2;
public class Client {
public static void main(String[] args) {
Invoice invoice = new Invoice(new Product("Product 1", 100), 5);
InvoicePrinter invoicePrinter = new InvoicePrinter(invoice);
invoicePrinter.printInvoice();
InvoiceSaver invoiceSaver = new DBInvoiceSaver(invoice);
invoiceSaver.saveInvoice();
}
}

Liskov Substitution Principle

Every Parent Class should be replaceable by it’s sub-class. If a class B extends a class A — then it should be possible to replace object of class A will object of class B without breaking any of the client’s behavior. In simple words, child class should extend the capability of parent class and not narrow it down.

version (v0)

package solid.l.v0;

public interface Bird {
void fly();
void eat();
}
package solid.l.v0;
public class Eagle implements Bird {
@Override
public void fly() {
System.out.println("Eagle is flying");
}
@Override
public void eat() {
System.out.println("Eagle is eating");
}
}
package solid.l.v0;
public class Penguin implements Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
@Override
public void eat() {
System.out.println("Penguin is eating");
}
}
package solid.l.v0;
public class Client {
public static void main(String[] args) {
Bird eagle = new Eagle();
Bird penguin = new Penguin();
eagle.fly();
penguin.fly();
}
}
// Here - we can see that this is clear violation of Liskov Substitution Principle.
// The Client class is using the fly() method of the Bird class. But, the Penguin class is not implementing it and throwing an exception.
// Problem is that Penguin is not a Flyable, but it is a Bird.
// So, we need to create a new interface Flyable and make Eagle implement it. Then, we need to change the Client class to cast the eagle to Flyable.

version (v1)

package solid.l.v1;

public abstract class Bird {
private String name;
private String color;
public abstract void eat();
}
package solid.l.v1;
public interface Flyable {
void fly();
}
package solid.l.v1;
public class Eagle extends Bird implements Flyable {
@Override
public void fly() {
System.out.println("Eagle is flying");
}
@Override
public void eat() {
System.out.println("Eagle is eating");
}
}
package solid.l.v1;
public class Penguin extends Bird {
@Override
public void eat() {
System.out.println("Penguin is eating");
}
}
package solid.l.v1;
public class Client {
public static void main(String[] args) {
Bird eagle = new Eagle();
Bird penguin = new Penguin();
eagle.eat();
((Flyable) eagle).fly();
penguin.eat();
}
}

Interface Segregation Principle

Clients should not be forced to implement methods that they don’t use. Try to make your interfaces narrow enough that client classes don’t have to implement behaviors they don’t need.

version (v0)

package solid.i.v0;
import java.io.File;
import java.util.List;
public interface CloudProvider {
public void addServer(String region, String serverId);
public List<String> listServers(String region);
public String getCDNAddress();
public void uploadFile(String name);
public File getFile(String name);
}
package solid.i.v0;
import java.io.File;
import java.util.*;
public class AWSProvider implements CloudProvider {
Map<String, List<String>> serversMap = new HashMap<>();
@Override
public void addServer(String region, String serverId) {
serversMap.computeIfAbsent(region, k -> new ArrayList<>()).add(serverId);
}
@Override
public List<String> listServers(String region) {
return serversMap.getOrDefault(region, Collections.emptyList());
}
@Override
public String getCDNAddress() {
return "xx.xx.xx.xx";
}
@Override
public void uploadFile(String name) {
System.out.println("Uploading file " + name + " to AWS");
}
@Override
public File getFile(String name) {
return new File(name);
}
}
package solid.i.v0;
import java.io.File;
import java.util.List;
public class DropboxProvider implements CloudProvider {
@Override
public void addServer(String region, String serverId) {
throw new UnsupportedOperationException("Can not add server to Dropbox");
}
@Override
public List<String> listServers(String region) {
throw new UnsupportedOperationException("Can not list servers in Dropbox");
}
@Override
public String getCDNAddress() {
throw new UnsupportedOperationException("Dropbox does not support CDN");
}
@Override
public void uploadFile(String name) {
System.out.println("Uploading file " + name + " to Dropbox");
}
@Override
public File getFile(String name) {
return new File(name);
}
}
// Here we are breaking the ISP principle by forcing the DropboxProvider to implement methods that it does not need.
// So, we should segregate the CloudProvider interface into smaller interfaces that are specific to each provider.
// - CloudHostingProvider, CloudStorageProvider, CDNProvider
// Then SalesforceProvider can implement only the CloudHostingProvider interface and DropboxProvider can implement only the CloudStorageProvider interface while AWSProvider can implement all three interfaces.

version (v1)

package solid.i.v1;
import java.util.List;
public interface CloudHostingProvider {
public void addServer(String region, String serverId);
public List<String> listServers(String region);
}
package solid.i.v1;
import java.io.File;
public interface CloudStorageProvider {
public void uploadFile(String name);
public File getFile(String name);
}
package solid.i.v1;
public interface CDNProvider {
public String getCDNAddress();
}
package solid.i.v1;
import java.io.File;
import java.util.*;
public class AWSProvider implements CloudHostingProvider, CloudStorageProvider, CDNProvider {
Map<String, List<String>> serversMap = new HashMap<>();
@Override
public void addServer(String region, String serverId) {
serversMap.computeIfAbsent(region, k -> new ArrayList<>()).add(serverId);
}
@Override
public List<String> listServers(String region) {
return serversMap.getOrDefault(region, Collections.emptyList());
}
@Override
public String getCDNAddress() {
return "xx.xx.xx.xx";
}
@Override
public void uploadFile(String name) {
System.out.println("Uploading file " + name + " to AWS");
}
@Override
public File getFile(String name) {
return new File(name);
}
}
package solid.i.v1;
import java.io.File;
public class DropboxProvider implements CloudStorageProvider {
@Override
public void uploadFile(String name) {
System.out.println("Uploading file " + name + " to Dropbox");
}
@Override
public File getFile(String name) {
return new File(name);
}
}

Dependency Inversion Principle

High-level classes shouldn’t depend on low-level classes. Both should depend on abstractions. Abstractions shouldn’t depend on details. Details should depend on abstractions.

In short, Class should depend on interfaces rather than concrete classes.

version (v0)

package solid.d.v0;

public abstract class Keyboard {
}
package solid.d.v0;
public class BluetoothKeyboard extends Keyboard {
}
package solid.d.v0;
public class WiredKeyboard extends Keyboard {
}
package solid.d.v0;
public abstract class Mouse {
}
package solid.d.v0;
public class BluetoothMouse extends Mouse {
}
package solid.d.v0;
public class WiredMouse extends Mouse {
}
package solid.d.v0;
public class Laptop {
private WiredKeyboard wiredKeyboard;
private WiredMouse wiredMouse;
public Laptop(WiredKeyboard wiredKeyboard, WiredMouse wiredMouse) {
this.wiredKeyboard = wiredKeyboard;
this.wiredMouse = wiredMouse;
}
}
// Here we have a class Laptop that depends on a WiredKeyboard and WiredMouse. The low level classes are directly dependent on other low level classes.
// and thus the classes are tightly coupled. This is a violation of the Dependency Inversion Principle.

version (v1)

package solid.d.v1;

import solid.d.v0.Keyboard;
import solid.d.v0.Mouse;
public class Laptop {
private Keyboard keyboard;
private Mouse mouse;
public Laptop(Keyboard keyboard, Mouse mouse) {
this.keyboard = keyboard;
this.mouse = mouse;
}
}
// By doing this, the direction of original dependency is inverted: low level classes are now dependent on high level abstractions.
// This is how we achieve Dependency Inversion Principle.
// - The high level modules should not depend on low level modules. Both should depend on abstractions.
package solid.d.v1;
import solid.d.v0.BluetoothKeyboard;
import solid.d.v0.BluetoothMouse;
import solid.d.v0.Keyboard;
import solid.d.v0.Mouse;
public class Client {
public static void main(String[] args) {
Keyboard keyboard = new BluetoothKeyboard();
Mouse mouse = new BluetoothMouse();
Laptop laptop = new Laptop(keyboard, mouse);
System.out.println(laptop);
}
}

Thank you so much for reading this article. If you found this helpful, please do share it with others and on social media. It would mean a lot to me.

Stay tuned and follow me for more content on System Design (LLD and HLD)

https://www.linkedin.com/in/ishan-aggarwal/

https://ishanaggarwal.substack.com/

--

--

Ishan Aggarwal

Consulting Principal MTS @ Oracle Cloud Infrastructures | Works on designing highly Scalable and Distributed Systems