SOLID Design Principles
Brief
Here is the excerpt from the post Principles of OOD - By Robert C Martin(Uncle Bob)
Acronym Expansion Goal SRP Single Responsibility Principle A class should have one, and only one, reason to change OCP Open Closed Principle You should be able to extend a classes behavior, without modifying it LSP The Liskov Substitution Principle Derived classes must be substitutable for their base classes ISP The Interface Segregation Principle Make fine grained interfaces that are client specific DIP The Dependency Inversion Principle Depend on Abstractions, not on concretions
Details
- Single Responsibility Principle
-
A class should have one and only one reason to change, meaning that a class should have only one job
-
When your class/function is doing more than one thing, it’s time create another class/function
-
Remove the unrelated material from the class
- Following this principle helps us to write better clean, and maintainable code, also it has got the following benefits
- Readability:
- SRP ensures that your code is clean and readable at all times, because your classes have just one responsibility
- Testability:
- SRP ensure that your classes are easier to test, because they just have only one responsibility
- Reusability:
- As your code is well tested following the SRP, it’s now easy to distribute it so that it can be reused
- Maintainability:
- Easier to maintain code that follows SRP, because whenever there is a change it has a specific purpose, and it matches the class’s responsibility, which help to maintain the code and keep the class aligned with its responsibility
- Readability:
-
Example:
-
Problem
-
class Person { String name; Person(String name) { this.name = name; } Person getPerson(String name) { /* DB Lookup: searches for the person with given name */ return person; } boolean savePerson(Person person) { /* DB Lookup: saves the person to some persistent storage */ return true; } }
- The class
Person
doesn’t honor SRP because, it contains database operations likegetPerson()
andsavePerson()
. We have to refactor this code to separate the concerns and abide to SRP
-
-
Solution
-
class Person { String name; Person(String name) { this.name = name; } }
-
class PersonDb { Person getPerson(String name) { /* DB Lookup: searches for the person with given name */ return person; } boolean savePerson(Person person) { /* DB Lookup: saves the person to some persistent storage */ return true; } }
- Now, the 2 classes have single responsibility, and they have only one reason to change
- BEFORE
- AFTER
-
-
-
- Open-Closed Principle
- Open for extension and closed for modification
- Your class/module should be open for extension and closed for modification. It doesn’t mean, you should never modify your original classes.
The Open Closed principle originally had to do with libraries and the fact that if you wanted to change a small part of behavior of that library, you had to actually change the library code which is poor practice. The open-closed principle was simply proposed to say that the main code we use from libraries should be closed for modification and open for inheritance, so that client code can use and/or modify the library’s behavior without having to alter its code. That’s really it. It’s of course not exclusive to libraries either and can apply to any publicly usable code whether from somebody else or from sections of code in the same codebase.
-
Example:
-
Problem
-
// Pseudo code class TicketPrice { double getDiscountedPrice() { if(customer.getType().equals("VIP")) { return price * 0.25; } else if (customer.getType().equals("VVIP")) { return price * 0.15; } } }
- If we’ve got a new requirement to add a discounted price for family, changing the method
getDiscountedPrice()
violates the OCP, because it should be closed for further modifications
-
-
Solution
-
// Pseudo code interface TicketPrice { default double getDefaultPrice() { return 0.2; } } class VipDiscounts implements TicketPrice { @override double getDefaultPrice() { return TicketPrice.super.getDefaultPrice() * 0.12; } } class VVipDiscounts implements TicketPrice { @override double getDefaultPrice() { return TicketPrice.super.getDefaultPrice() * 0.01; } } class FamilyDiscounts implements TicketPrice { @override double getDefaultPrice() { return TicketPrice.super.getDefaultPrice() * 0.5; } }
- The code above abides to OCP because it hasn’t really modified the existing code rather it has extended
- Note: It’s not a law or rule that you should always extend classes/modules as said in OCP, it was told it a context where the original classes are shared as a library wherein we cannot change the source code in such cases we have no option other than extended it to add customized behavior, however this cannot be applied for tiny changes like adding a new field to original class if you have permission to change the original class you can do it, otherwise for this small change if you try to extend it by creating new class it results in unnecessary abstraction hell.
- BEFORE
- AFTER
-
-
-
- Liskov Substitution Principle
-
Reference If
S
is a subtype ofT
, then objects of typeT
in a program may be replaced with objects of typeS
without altering any of the desirable properties of that program -
Subtypes must be substitutable for their base types without altering the correctness of the program
- Example
-
Problem
-
public abstract class Bird { public abstract void fly(); public void eat() { // Eats insects, seeds, and fish } } public Parrot extends Bird { @Override public void fly() { // TODO: Parrot is flying } } public Ostrich extends Bird { @Override public void fly() { throw new UnSupportedException("Ostrich doesn't fly!"); } }
- This is an example where polymorphism has gone wrong. The problem with the above code snippet is that you probably have the wrong abstraction, because if you pass ostrich object to a function that accepts Bird types, and if you call fly method on ostrich object it would result in error as Ostrich doesn’t fly and is not a flying bird.
-
-
Solution
-
public abstract class Bird { public void eat() { // Eats insects, seeds, and fish } } public abstract class FlyingBird extends Bird { public abstract void fly(); } public class Parrot extends FlyingBird { @Override public void fly() { // TODO: Parrot is flying } } public class Ostrich extends Bird {}
- LSP fixes the problem by creating the correct abstractions.
-
Now that we have taken care of
fly()
method being abstracted toFlyingBird
, and we have ensured that non-flying birds can’t callfly()
- Substitutability
public class BirdsUtil { public void doEat(Bird bird) { bird.eat(); } public void doFly(FlyingBird flyingBird) { flyingBird.fly(); } }
- This is the core of LSP: subtypes can be substituted for their base types without altering the correctness of the program.
- LSP helps prevent misuse of polymorphism by ensuring proper behavioral substitution.
- BEFORE
- AFTER
-
-
-
- Interface segregation Principle
- Client should not be forced to depend on interfaces which they do not use
- Segregate your interfaces meaning just have more than one inheritance hierarchies in case if your interfaces are not highly cohesive
- Cohesion: Talk about how closely the class data and methods are related. Highly cohesive classes/interfaces are well focussed, easier to maintain and can be easier to extend.
- Example:
- Problem
-
interface VendingMachine { default void printDeliveryReceipt() { // add default implementation } /* Snack */ boolean dispenseSnack(); /* Hot Beverages */ boolean brewCoffee(); boolean brewTea(); /* Cold Beverages */ boolean dispenseWater(); boolean dispenseCoke(); }
-
class SnackMachine implements VendingMachine { @Override public void printDeliveryReceipt() { // TODO: Prints Delivery Receipt } @Override public boolean dispenseSnack() { // TODO: Implement code to dispense snack item return true; } // Below are unrelated overrides @Override public boolean brewCoffee() { // Irrelevant for SnackMachine } @Override public boolean brewTea() { // Irrelevant for SnackMachine } @Override public boolean dispenseWater() { // Irrelevant for SnackMachine } @Override public boolean dispenseCoke() { // Irrelevant for SnackMachine } }
- The class
SnackMachine
is forced to implement unrelated methods, even though it doesn’t make sense. This violates the Interface Segregation Principle.
-
-
Solution
-
The better approach to solve this is to segregate the relevant methods into interfaces as shown below
-
public interface VendingMachine { default void printDeliveryReceipt() { // add default implementation } }
-
public interface SnackMachine extends VendingMachine { boolean dispenseSnack(); }
-
public interface HotBeverageMachine extends VendingMachine { boolean brewTea(); boolean brewCoffee(); }
-
public interface ColdBeverageMachine extends VendingMachine { boolean dispenseWater(); boolean dispenseCoke(); }
-
Now, if we want to create a snack machine it would only have to implement
SnackMachine
interface as shown below-
public class SnackMachineImpl implements SnackMachine { @Override public void printDeliveryReceipt() { // TODO: Prints Delivery Receipt } @Override public boolean dispenseSnack() { // TODO: Implement code to dispense snack item randomly return true; } }
- Here, the
SnackMachineImpl
class doesn’t override unrelated methods, because we have segregated the interface accordingly enabling each one of the interface to be highly cohesive meaning the methods in the interfaces are closely related to the domain. - BEFORE
- AFTER
-
-
- Problem
- LSP vs ISP
Both are related but different concepts.
LSP talks about subtypes design while ISP emphasizes on basetypes design
ISP is about segregating interfaces in other words breaking down bloated interface into multiple interfaces.
LSP’s goal is to ensure behavioral correctness without any exceptions when the subtypes are substituted for based types.
- Dependency inversion Principle
-
Depend on abstractions not on concretions
-
High-level modules should not depend on low-level modules, both should depend on abstractions(E.g., interfaces)
- Example
- Problem
-
class BillPaymentService { // High-level module Paytm paytm; BillPaymentService(Paytm paytm) { this.paytm = paytm; } boolean makePayment(double amount) { return paytm.makePayment(amount); } }
-
class Paytm { // Low-level module Payee payee; Paytm(Payee payee) { this.payee = payee; } boolean makePayment(double amount) { // TODO: Paytm specific payment code return true; } }
-
class Payee { String payeeName; long accountNumber; Payee(String payeeName, long accountNumber) { this.payeeName = payeeName; this.accountNumber = accountNumber; } }
-
// A sample client or driver code class Client { public static void main(String args[]) { Payee payee = new Payee("South Power Distribution", 123456); Paytm paytm = new paytm(payee); BillPaymentService billPaymentService = new BillPaymentService(paytm); billPaymentService.makePayment(12.5); } }
- Here
BillPaymentService
a High-level module is dependent onPaytm
a low-level module where actual payment happens. Below is the sample representation of this.-
BillPaymentService(High-level module) -> Paytm(Low-level module)
-
- As per this DI, High-level modules should not directly depend on Low-level modules, rather both should depend on abstractions(E.g., Interfaces), so it clearly violates the DI principle.
-
- Solution
-
interface UPI { boolean makePayment(double amount); }
-
// High-level module is dependent on UPI(Abstraction) through composition class BillPaymentService { UPI upi; BillPaymentService(UPI upi) { this.upi = upi; } boolean makePayment(double amount) { return upi.makePayment(amount); } }
-
// Low-level module is dependent on UPI(Abstraction) inversely through inheritance class Paytm implements UPI { Payee payee; Paytm(Payee payee) { this.payee = payee; } @Override boolean makePayment(double amount) { // TODO: Paytm specific payment code return true; } }
-
class Payee { String payeeName; long accountNumber; Payee(String payeeName, long accountNumber) { this.payeeName = payeeName; this.accountNumber = accountNumber; } }
-
// A sample client or driver code class Client { public static void main(String args[]) { Payee payee = new Payee("South Power Distribution", 123456); UPI upi = new paytm(payee); BillPaymentService billPaymentService = new BillPaymentService(upi); billPaymentService.makePayment(12.5); } }
- The above code adheres to dependency inversion principle i.e.,
BillPaymentService
a high level module is dependent on abstraction i.e.,UPI
interface andPaytm
a low level module is inversely dependent on abstractionUPI
- The significant advantage of the solution above is you can inject any dependency/subType of UPI to
BillPaymentService
. This is called Dependency Injection -
Note: Dependency Inversion Principle(DIP) and Dependency Injection(DI) are closely related but different concepts.
-
DIP talks about what, DI talks about how.
-
DI is one way to implement DIP. The above example uses Constructor injection to abide to DIP
- BEFORE
- AFTER
-
- Problem
-