SOLID Principles
The SOLID principles are a foundational set of five object-oriented design rules.
- S → Single Responsibility Principle (SRP)
- O → Open/Closed Principle (OCP)
- L → Liskov Substitution Principle (LSP)
- I → Interface Segregation Principle (ISP)
- D → Dependency Inversion Principle (DIP)
1. Single Responsibility Principle (SRP)
Section titled “1. Single Responsibility Principle (SRP)”“A class should have one, and only one, reason to change.”
- Goal: To create highly cohesive classes. If a class handles multiple responsibilities, changing one part of the logic might inadvertently break another.
Bad Implementation
Section titled “Bad Implementation”Imagine an Employee class that holds personal details, calculates taxes, and writes data directly to a database.
public class Employee { private String name; private double salary;
// Responsibility 1: Business Logic public double calculateTax() { ... }
// Responsibility 2: Database Logic public void saveToDB() { ... }
// Responsibility 3: Presentation Logic public void printReport() { ... }}This class is tightly coupled. If the database schema changes, or the tax law changes, or the reporting format changes, this single class must be modified.
Good Implementation
Section titled “Good Implementation”
Split the monolithic God Class into smaller, focused classes.
We delegate responsibilities to dedicated classes. This makes the code loosely coupled, easily testable, and limits the reasons to modify any single file.
// Handles only datapublic class Employee { private String name; private double salary;}
// Handles only business logicpublic class TaxCalculator { public double calculateTax(Employee e) { ... }}
// Handles only database operationspublic class EmployeeRepository { public void save(Employee e) { ... }}2. Open/Closed Principle (OCP)
Section titled “2. Open/Closed Principle (OCP)”“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
- Goal: To allow developers to add new features without altering existing, tested, and working code, thereby reducing the risk of introducing regressions.
Bad Implementation
Section titled “Bad Implementation”A SalaryCalculator that uses conditional logic to determine behavior based on an employee type.
public class SalaryCalculator { public double calculate(Employee e) { if (e.getType().equals("PERMANENT")) { return e.getBase() + e.getBonus(); } else if (e.getType().equals("CONTRACT")) { return e.getHours() * e.getRate(); } return 0; }}Issue: If a new employee type (e.g., PartTime) is introduced, you must modify the calculate method, violating OCP.
Good Implementation
Section titled “Good Implementation”
Use polymorphism. An interface defines the contract, and new features are added by creating new classes.
Create an interface and let child classes implement their specific logic. Now, adding a PartTimeEmployee requires zero changes to existing files.
public interface Employee { double calculateSalary();}
public class PermanentEmployee implements Employee { public double calculateSalary() { return base + bonus; }}
public class ContractEmployee implements Employee { public double calculateSalary() { return hours * rate; }}3. Liskov Substitution Principle (LSP)
Section titled “3. Liskov Substitution Principle (LSP)”“Objects of a superclass shall be replaceable with objects of its subclasses without breaking the application.”
- Goal: To ensure inheritance is used correctly. If a program expects a parent class, it should be able to work flawlessly with any child class.
Bad Implementation
Section titled “Bad Implementation”A ToyCar extending a Car class but failing to fulfill the contract of a “Car” because toys don’t use real fuel.
public class Car { public void startEngine() { ... } public void addFuel() { ... }}
public class ToyCar extends Car { @Override public void addFuel() { throw new UnsupportedOperationException("Toy cars don't use fuel!"); }}If a piece of code accepts a Car and calls .addFuel(), substituting it with a ToyCar will crash the application.
Good Implementation
Section titled “Good Implementation”
Refactor the hierarchy so subclasses only inherit what they can actually fulfill.
public interface Vehicle { void startEngine();}
public class FuelVehicle implements Vehicle { public void startEngine() { ... } public void addFuel() { ... }}
public class ToyVehicle implements Vehicle { public void startEngine() { ... } public void replaceBattery() { ... }}4. Interface Segregation Principle (ISP)
Section titled “4. Interface Segregation Principle (ISP)”“No client should be forced to depend on methods it does not use.”
- Goal: To avoid creating large, monolithic interfaces (“fat interfaces”). Interfaces should be small and highly specific.
Bad Implementation
Section titled “Bad Implementation”A massive Machine interface forced onto a basic printer that cannot scan or fax.
public interface Machine { void print(); void scan(); void fax();}
public class BasicPrinter implements Machine { public void print() { /* valid implementation */ }
public void scan() { throw new NotSupportedException(); } public void fax() { throw new NotSupportedException(); }}Good Implementation
Section titled “Good Implementation”
Break the fat interface into smaller, role-specific interfaces.
public interface IPrinter { void print(); }public interface IScanner { void scan(); }public interface IFax { void fax(); }
// A basic printer only implements what it needspublic class BasicPrinter implements IPrinter { public void print() { ... }}
// A smart machine can implement multiple interfacespublic class AllInOneMachine implements IPrinter, IScanner, IFax { public void print() { ... } public void scan() { ... } public void fax() { ... }}5. Dependency Inversion Principle (DIP)
Section titled “5. Dependency Inversion Principle (DIP)”“High-level modules should not depend on low-level modules. Both should depend on abstractions.”
- Goal: To decouple your application’s core logic from specific infrastructure details (like a specific database vendor or third-party API).
Bad Implementation
Section titled “Bad Implementation”A high-level OrderService directly instantiating a low-level concrete MySQLRepository.
public class OrderService { // Tight coupling to a specific database implementation private MySQLRepository repository = new MySQLRepository();
public void checkout(Order order) { repository.save(order); }}Issue: If you want to migrate to a NoSQL database, you have to rewrite the OrderService. It is impossible to unit test OrderService without an actual MySQL database running.
Good Implementation
Section titled “Good Implementation”
Introduce an interface layer. The high-level service depends on the interface, and the low-level database implements it.
// 1. Define the abstractionpublic interface IRepository { void save(Order order);}
// 2. Low-level module implements abstractionpublic class MySQLRepository implements IRepository { public void save(Order order) { ... }}
// 3. High-level module depends on abstraction via Dependency Injectionpublic class OrderService { private IRepository repository;
// Injected via constructor public OrderService(IRepository repository) { this.repository = repository; }
public void checkout(Order order) { repository.save(order); }}