SOLID Principles in OOD
SOLID Principles Overview
- SOLID principles are a set of rules that help developers design maintainable and scalable software systems.
- Introduced by Robert C. Martin "Uncle Bob" in 2000 and the acronym SOLID introduced by Michael Feathers in 2004.
- SOLID stands for:
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Why SOLID?
Encourage the creation of more maintainable, understandable, and flexible software.
Each principle offers guidelines for creating code that is easier to read, modify, and test.
Reduce complexity as applications grow.
Aims to write code that satisfies present and future requirements.
SOLID was introduced to address problems like:
- Rigidity: Changes in one part of the program break another.
- Fragility: Breakage occurs in unrelated places.
- Immobility: Code cannot be reused outside its original context.
Single Responsibility Principle (SRP)
- A class should have only one reason to change.
- Each class should have a single responsibility or job to perform.
- Example: A class A which does the following operations:
- Opens a database connection,
- Fetches data from the database, and
- Writes the data in an external file.
- Issue: Class handles too many operations; changes (e.g., new database, output structure) may affect multiple operations.
Why Follow SRP?
- Easier to understand, modify, and test classes.
- Testing: Fewer test cases needed for a class with one responsibility.
- Lower coupling: Less functionality reduces dependencies.
- Organization: Smaller, well-organized classes are easier to search and understand.
How to Implement SRP
- Example 1: Book Class
- Problem: A
Bookclass that not only stores book data (name, author, text) but also handles printing to the console violates SRP. - Solution: Create a separate
BookPrinterclass to handle printing responsibilities. - Benefits: Relieves
Bookclass of printing duties and allowsBookPrinterto be used for other output media (email, logging, etc.).
- Problem: A
- Example 2: Shape Classes
- Problem: A
CalculateAreasclass calculates areas of different shapes and handles output. This couples area calculation and output behavior. - Solution: Separate the output behavior into an
OutputAreasclass. - Benefits: Allows changes to the output method without requiring recompilation of the area calculation code. Improves testability by separating responsibilities.
- Problem: A
- Example 3: BankService Class
- Problem: A
BankServiceclass handles multiple unrelated actions (deposit, withdrawal, loan info, printing passbook, sending notifications). - Solution: Segregate responsibilities into separate services such as
BankService,LoanService,PrinterService, andNotificationService. - Benefits: Code becomes clearer and more understandable, with each class performing its own distinct actions.
- Problem: A
Code Examples for SRP
Example 1 - BadBook Class:
public class BadBook { //... void printTextToConsole() { // our code for formatting and printing the text } }Example 1 - BookPrinter Class:
public class BookPrinter { // methods for outputting text void printTextToConsole(String text) { //our code for formatting and printing the text }
}void printTextToAnotherMedium(String text) { // code for writing to any other location.. }Example 2 - CalculateAreas and OutputAreas Classes:
class CalculateAreas { Shape[] shapes; double sumTotal = 0;
} class OutputAreas { double areas = 0;public CalculateAreas(Shape[] sh) { this.shapes = sh; } public double sumAreas() { sumTotal = 0; for (int i = 0; i < shapes.length; i++) { sumTotal = sumTotal + shapes[i].calcArea(); } return sumTotal; }
}public OutputAreas(double a) { this.areas = a; } public void console() { System.out.println("Total of all areas = " + areas); } public void HTML() { System.out.println("<HTML>"); System.out.println("Total of all areas = " + areas); }Example 3 - Segregated Bank Service Classes:
public class BankService { // Bank Services public void withdraw(double amount) { System.out.println("Withdraw : " + amount); }
} public class LoanService { // Loan Services public String getLoanInfo(String loanType) { if (loanType.equals("professional")) { return "Professional Loan"; } else if (loanType.equals("home")) { return "Home Loan"; } else { return "Personal Loan"; } } } public class PrinterService { // Printer Services public void printPassbook() { System.out.println("Printing Book Details..."); } } public class NotificationService { // Notification Services public void sendOTP(String medium) { if (medium.equals("mobile")) { System.out.println("Sending OTP to mobile"); } else if (medium.equals("email")) { System.out.println("Sending OTP to email"); } } }public void deposit(double amount) { System.out.println("Deposit : " + amount); }
Open-Closed Principle (OCP)
- A class should be open for extension but closed for modification.
- Developers should be able to add new functionality without changing existing code (unless there is a bug).
- Accommodate new features by extending classes, not modifying them.
- Proposals to implement include:
- A new class inheriting from the old one.
- A Polymorphic approach in which we could inherit from an abstract base class.
Why Follow OCP?
- Systems become more flexible and easier to maintain.
- Reduces the risk of introducing new bugs into existing functionality.
How to Implement OCP
- Example 1: Guitar Class
- Problem: Adding a flame pattern to an existing
Guitarclass by modifying it directly may introduce errors. - Solution: Extend the
Guitarclass with aSuperCoolGuitarWithFlamesclass. - Benefits: Ensures existing application functionality remains unaffected.
- Problem: Adding a flame pattern to an existing
- Example 2: ShapeCalculator Class
- Problem: A
ShapeCalculatorclass that only calculates the area of aRectanglerequires modification to support other shapes likeCircle. - Solution: Use an abstract
Shapeclass with an abstractgetArea()method, whichRectangleandCircleclasses then inherit. - Benefits: New shapes can be added without modifying the
ShapeCalculatorclass.
- Problem: A
- Example 3: NotificationService Class
- Problem: A
NotificationServiceclass sending OTP notifications via mobile and email requires modification to add WhatsApp support. - Solution: Refactor
NotificationServiceinto an interface, then implementMobileNotificationService,EmailNotificationService, andWhatsAppNotificationServiceclasses. - Benefits: Adding new notification methods only requires creating a new service, extending from
NotificationService, without modifying existing classes.
- Problem: A
Code Examples for OCP
Example 1 - Extending Guitar Class:
public class SuperCoolGuitarWithFlames extends Guitar { private String flameColor; //constructor, getters + setters }Example 2 - Shape and CalculateAreas Classes:
abstract class Shape { public abstract double getArea(); } class Rectangle extends Shape { protected double length; protected double width;
} class Circle extends Shape { protected double radius;public Rectangle(double l, double w) { length = l; width = w; } public double getArea() { return length * width; }
} class CalculateAreas { private double area;public Circle(double r) { radius = r; } public double getArea() { return radius * radius * 3.14; }
}public double calcArea(Shape s) { area = s.getArea(); return area; }Example 3 - Notification Services:
public interface NotificationService { void sendOTP(String medium);
} public class MobileNotificationService implements NotificationService { @Override public void sendOTP(String medium) { System.out.println("Sending OTP Number Message to: " + medium); }void sendTransactionHistory(String medium);
} public class EmailNotificationService implements NotificationService { @Override public void sendOTP(String medium) { System.out.println("Sending OTP Number Email to: " + medium); }@Override public void sendTransactionHistory(String medium) { System.out.println("Sending Transactions Message to: " + medium); }
} public class WhatsAppNotificationService implements NotificationService { @Override public void sendOTP(String medium) { System.out.println("Sending OTP Number to: " + medium); }@Override public void sendTransactionHistory(String medium) { System.out.println("Sending Transactions Email to: " + medium); }
}@Override public void sendTransactionHistory(String medium) { System.out.println("Sending Transactions Details to: " + medium); }
Liskov Substitution Principle (LSP)
- Derived classes must be completely substitutable for their base classes.
- Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
- If a parent can do something, a child must also be able to do it.
- If class A is a subtype of class B, we should be able to replace B with A without disrupting the behavior of our program.
- If q(x) is a property provable about any object x of type T, then q(y) should be provable for any object y of type S, where S is a subtype of T.
How to Implement LSP
- Example 1: Social Media Platforms
- Problem: A SocialMedia class with methods like chat(), publish(), and groupCall() is inherited by Facebook, Instagram, and WhatsApp. WhatsApp doesn't support publish(), and Instagram doesn't support groupCall(). This violates LSP because WhatsApp and Instagram cannot be completely substitutable for the SocialMedia class.
- Solution: Separate interfaces for different responsibilities: SocialMedia (chat()), PostManager (publish()), and VideoCallManager (groupCall()). Implement these interfaces in classes that support the relevant actions.
- Benefits: Each subclass (Facebook, WhatsApp) performs only the actions it supports, adhering to LSP.
- Example 2: Shape Hierarchy
- Problem: Square inherits from Rectangle, but the constructor of Square only requires one parameter (side), while Rectangle expects two (length and width). Also, the calcArea() functionality is subtly different.
- Solution: Make Square inherit directly from Shape instead of Rectangle.
- Benefits: Enforces consistency and ensures a square is not forced to act like a rectangle.
- Example 3: Animal Class
- Problem: A DumbDog class extends Animal but throws an exception in the makeNoise() method because it can't bark. This violates LSP.
- Solution: Ensure all subclasses can perform the actions defined in the parent class, or reconsider the class hierarchy.
Code Examples for LSP
Example 1 - Social Media Interfaces and Classes:
public interface SocialMedia { void chat(String user); } public interface PostManager { void publish(Object post); } public interface VideoCallManager { void groupCall(String... users); } public class Facebook implements SocialMedia, PostManager, VideoCallManager { @Override public void publish(Object post) { System.out.println("Publishing a post on Facebook: " + post); }
} public class WhatsApp implements SocialMedia, VideoCallManager { @Override public void chat(String user) { System.out.println("Chatting on WhatsApp with: " + user); }@Override public void chat(String user) { System.out.println("Chatting on Facebook with: " + user); } @Override public void groupCall(String... users) { System.out.println("Taking a Group Call on Facebook with: " + Arrays.toString(users)); }
}@Override public void groupCall(String... users) { System.out.println("Taking a Group Call on WhatsApp with: " + Arrays.toString(users)); }Example 2 - Corrected Shape Classes:
abstract class Shape { protected double area;
} class Rectangle extends Shape { private double length; private double width;public abstract double calcArea();
} class Square extends Shape { private double side;public Rectangle(double l, double w) { length = l; width = w; } public double calcArea() { area = length * width; return (area); }
}public Square(double s) { side = s; } public double calcArea() { area = side * side; return (area); }
Interface Segregation Principle (ISP)
- Software clients should not be forced to depend on interfaces they do not use.
- Interfaces should be small, focused, and specific to their needs.
- Larger interfaces should be split into smaller ones.
- Implementing classes only need to be concerned about methods that are of interest to them.
Why Follow ISP?
- Systems become more modular and easier to maintain.
- Promotes the use of composition over inheritance, leading to more flexible and reusable code.
How to Implement ISP
- Example 1: BearKeeper Interface
- Problem: A BearKeeper interface includes washTheBear(), feedTheBear(), and petTheBear(). Not all zookeepers are comfortable petting bears.
- Solution: Split the large interface into three separate interfaces: BearCleaner, BearFeeder, and BearPetter.
- Benefits: Allows zookeepers to implement only the interfaces relevant to their roles.
- Example 2: IMammal Interface
- Problem: A single IMammal interface includes eat() and makeNoise(). Some mammals might not eat in the traditional sense.
- Solution: Separate the behaviors into separate interfaces: IEat and IMakeNoise.
- Benefits: Allows each mammal class to implement only the interfaces relevant to its behavior.
Code Examples for ISP
Example 1 - Segregated Bear Interfaces:
public interface BearCleaner { void washTheBear(); } public interface BearFeeder { void feedTheBear(); } public interface BearPetter { void petTheBear(); } public class BearCarer implements BearCleaner, BearFeeder { public void washTheBear() { //I think we missed a spot... }
}public void feedTheBear() { //Tuna Tuesdays... }Example 2 - Mammal Interfaces:
interface IEat { public void eat(); } interface IMakeNoise { public void makeNoise(); } class Dog implements IEat, IMakeNoise { public void eat() { System.out.println("Dog is eating"); }
}public void makeNoise() { System.out.println("Dog is making noise"); }
Dependency Inversion Principle (DIP)
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Use abstraction (abstract classes and interfaces) instead of concrete implementations.
- It is better for high-level modules to avoid having dependencies on low-level modules; instead, both high- and low-level modules should depend on abstractions.
Why Follow DIP?
- Systems become more flexible and easier to maintain.
- Promotes the use of dependency injection, allowing for greater decoupling between components.
- Changes to either high-level or low-level modules will not affect the other, as long as they both depend on the same abstraction.
How to Implement DIP
- Example 1: Windows98Machine Class
- Problem: A Windows98Machine class is tightly coupled with StandardKeyboard and Monitor classes through the new keyword.
- Solution: Introduce a Keyboard interface and use this in the Windows98Machine class. Decouple the machine from the StandardKeyboard by using the Keyboard abstraction.
- Benefits: Easily switch out the type of keyboard in the machine with a different implementation of the interface and more straightforward testing.
Code Examples for DIP
Example 1 - Decoupled Windows98Machine:
public interface Keyboard { } public class Windows98Machine { private Keyboard keyboard; private Monitor monitor;
} public class StandardKeyboard implements Keyboard { }public Windows98Machine(Keyboard keyboard, Monitor monitor) { this.keyboard = keyboard; this.monitor = monitor; }
SOLID Principles - Summary
- You should be able to extend a class’s behavior without modifying it.
- A class should have only a single reason to change.
- The design must provide the ability to replace any instance of a parent class with an instance of one of its child classes.
- It is better to have many small interfaces than a few larger ones.
- Code should depend on abstractions.