SOLID Principles in Java
Introduction
The SOLID principles are a set of five design guidelines intended to make software designs more understandable, flexible, and maintainable. Coined by Robert C. Martin (Uncle Bob), these principles form the core philosophy of object-oriented design and architecture.
In Java, adhering to SOLID principles results in codebases that are highly modular, easy to test, and resilient to changing requirements.
1. Single Responsibility Principle (SRP)
"A class should have one, and only one, reason to change."
Every class or module should focus on a single piece of functionality. If a class takes on multiple responsibilities, they become coupled. Changing one responsibility might unexpectedly break the other.
Violation Example
public class Employee {
public void calculatePay() { /* ... */ }
public void saveToDatabase() { /* ... */ } // Violation: Database logic mixed with business logic
public void generateReport() { /* ... */ } // Violation: Presentation logic mixed in
}
Refactored
public class Employee {
// Only core employee attributes and behaviors
}
public class EmployeeRepository {
public void save(Employee emp) { /* ... */ }
}
public class EmployeeReportGenerator {
public void generate(Employee emp) { /* ... */ }
}
2. Open/Closed Principle (OCP)
"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification."
You should be able to add new functionality without changing existing code, reducing the risk of introducing new bugs into working systems. In Java, this is heavily achieved through interfaces and abstract classes.
Violation Example
public class DiscountCalculator {
public double calculateDiscount(String customerType, double amount) {
if (customerType.equals("VIP")) return amount * 0.2;
if (customerType.equals("REGULAR")) return amount * 0.1;
return 0; // What if we add a "GUEST" type? We must modify this class!
}
}
Refactored
public interface DiscountStrategy {
double calculate(double amount);
}
public class VipDiscount implements DiscountStrategy {
public double calculate(double amount) { return amount * 0.2; }
}
public class RegularDiscount implements DiscountStrategy {
public double calculate(double amount) { return amount * 0.1; }
}
Now, adding a new customer type simply means adding a new class that implements DiscountStrategy.
3. Liskov Substitution Principle (LSP)
"Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program."
If class B is a subclass of class A, we should be able to pass an object of class B to any method that expects an object of class A, and the method shouldn't behave weirdly.
Violation Example
The classic "Square-Rectangle" problem.
public class Rectangle {
protected int width, height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Violates expected behavior of a Rectangle
}
// ...
}
Refactored
Instead of inheritance, use a common interface if they don't share substitutable behaviors.
public interface Shape {
int getArea();
}
// Implement independently without forcing unnatural relationships
4. Interface Segregation Principle (ISP)
"Many client-specific interfaces are better than one general-purpose interface."
Clients should not be forced to depend on methods they do not use. Large interfaces should be split into smaller, more specific ones.
Violation Example
public interface MultiFunctionPrinter {
void print();
void scan();
void fax();
}
public class BasicPrinter implements MultiFunctionPrinter {
public void print() { /* implementation */ }
public void scan() { throw new UnsupportedOperationException(); } // Forced to implement
public void fax() { throw new UnsupportedOperationException(); } // Forced to implement
}
Refactored
public interface Printer {
void print();
}
public interface Scanner {
void scan();
}
public class BasicPrinter implements Printer {
public void print() { /* implementation */ }
}
public class AdvancedPrinter implements Printer, Scanner {
public void print() { /* implementation */ }
public void scan() { /* implementation */ }
}
5. Dependency Inversion Principle (DIP)
"Depend upon abstractions. Do not depend upon concrete classes."
High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces). Abstractions should not depend on details. Details should depend on abstractions.
Violation Example
public class LightBulb {
public void turnOn() { /* ... */ }
}
public class Switch {
private LightBulb bulb; // Tightly coupled to a specific implementation
public Switch() {
this.bulb = new LightBulb();
}
public void operate() {
bulb.turnOn();
}
}
Refactored
public interface Switchable {
void turnOn();
void turnOff();
}
public class LightBulb implements Switchable {
public void turnOn() { /* ... */ }
public void turnOff() { /* ... */ }
}
public class Switch {
private Switchable device; // Depends on abstraction
public Switch(Switchable device) { // Dependency Injection
this.device = device;
}
public void operate() {
device.turnOn();
}
}
This principle is the foundation for frameworks like Spring, which heavily utilize Dependency Injection (DI) and Inversion of Control (IoC).