SOLID Principles
The SOLID principles are five design principles that help developers write maintainable, scalable, and testable object-oriented code.
1. Single Responsibility Principle (SRP)
A class should have only one reason to change.
Example (Violating SRP):
This class handles both employee data and file operations.
class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public void saveToFile() {
// Code to save employee data to a file (bad practice)
}
}
Why is this bad?
• If we change how we store employees (e.g., from files to a database), we must modify this class.
Example (Following SRP):
Separate the responsibilities:
class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
}
class EmployeeFileManager {
public void saveToFile(Employee employee) {
// Code to save employee data
}
}
Now:
• Employee handles only employee data.
• EmployeeFileManager handles file storage.
2. Open/Closed Principle (OCP)
Classes should be open for extension, but closed for modification.
Example (Violating OCP):
class AreaCalculator {
public double calculate(Object shape) {
if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.getRadius() * c.getRadius();
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.getWidth() * r.getHeight();
}
return 0;
}
}
• Adding a new shape requires modifying AreaCalculator, violating OCP.
Example (Following OCP):
Use polymorphism so new shapes can be added without modifying existing code.
interface Shape {
double calculateArea();
}
class Circle implements Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
public double calculateArea() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double calculateArea() {
return width * height;
}
}
class AreaCalculator {
public double calculate(Shape shape) {
return shape.calculateArea();
}
}
Now, adding a new shape like Triangle does not modify AreaCalculator.
3. Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without breaking behavior.
Bad Example (Violating LSP):
class Bird {
public void fly() {
System.out.println("Flying...");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
Penguin breaks expectations because it overrides fly() incorrectly.
Example (Following LSP):
Separate flying and non-flying birds.
interface Bird { }
interface FlyingBird extends Bird {
void fly();
}
class Sparrow implements FlyingBird {
public void fly() {
System.out.println("Flying...");
}
}
class Penguin implements Bird {
public void swim() {
System.out.println("Swimming...");
}
}
Now, Penguin does not break expectations because it doesn’t pretend to fly.
4. Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
Example (Violating ISP):
interface Worker {
void work();
void eat();
}
class Robot implements Worker {
public void work() {
System.out.println("Working...");
}
public void eat() {
throw new UnsupportedOperationException("Robots don’t eat!");
}
}
• The Robot class is forced to implement eat(), which is meaningless.
Example (Following ISP):
Split interfaces into smaller, specific ones.
interface Worker {
void work();
void eat();
}
class Robot implements Worker {
public void work() {
System.out.println("Working...");
}
public void eat() {
throw new UnsupportedOperationException("Robots don’t eat!");
}
}
Now:
• Human implements both Workable and Eatable.
• Robot only implements Workable.
5. Dependency Inversion Principle (DIP)
Depend on abstractions, not concrete implementations.
Example (Violating DIP):
class Keyboard { }
class Computer {
private Keyboard keyboard;
public Computer() {
this.keyboard = new Keyboard(); // Direct dependency
}
}
• The Computer class is tightly coupled to Keyboard, making changes difficult.
Example (Following DIP):
Use dependency injection.
interface Keyboard { }
class MechanicalKeyboard implements Keyboard { }
class Computer {
private Keyboard keyboard;
public Computer(Keyboard keyboard) { // Inject dependency
this.keyboard = keyboard;
}
}
Now:
• Computer depends on an interface (Keyboard), allowing different keyboard types.
Summary Table
| Principle | Description | Fix |
|---|---|---|
| SRP | One reason to change | Separate concerns |
| OCP | Open for extension, closed for modification | Use polymorphism |
| LSP | Subclasses should not break base class behavior | Follow correct hierarchy |
| ISP | Avoid forcing classes to implement unused methods | Split interfaces |
| DIP | Depend on abstractions, not concrete classes | Use dependency injection |
