7.6 SOLID Principles
SOLID is an acronym for five core principles of object-oriented design, coined by Robert C. Martin ("Uncle Bob"). Following these principles produces code that is resilient to change, easy to understand, and highly reusable.
S - Single Responsibility Principle
"A class should have only one reason to change."
A class should have only one responsibility (function). When multiple responsibilities are mixed, changing one affects the others.
// ❌ SRP violation: one class handles everything related to users
class User {
String name;
String email;
void saveToDatabase() { /* DB logic */ } // DB responsibility
void sendWelcomeEmail() { /* email logic */ } // email responsibility
String formatUserInfo() { return name + " <" + email + ">"; } // format responsibility
}
// ✅ SRP compliant: separate each responsibility into its own class
class User {
String name;
String email;
}
class UserRepository { // DB responsibility
void save(User user) { System.out.println("Saved to DB: " + user.name); }
}
class EmailService { // email responsibility
void sendWelcome(User user) { System.out.println("Email sent: " + user.email); }
}
class UserFormatter { // format responsibility
String format(User user) { return user.name + " <" + user.email + ">"; }
}
O - Open/Closed Principle
"Software entities should be open for extension, but closed for modification."
New functionality should be added by extending existing code, not modifying it.
// ❌ OCP violation: adding a new discount type requires modifying calculateDiscount()
class DiscountCalculator {
double calculateDiscount(String type, double price) {
if (type.equals("VIP")) return price * 0.2;
if (type.equals("STUDENT")) return price * 0.1;
// Must edit here every time a new type is added!
return 0;
}
}
// ✅ OCP compliant: extend via interface
interface DiscountPolicy {
double calculate(double price);
}
class VipDiscount implements DiscountPolicy {
public double calculate(double price) { return price * 0.2; }
}
class StudentDiscount implements DiscountPolicy {
public double calculate(double price) { return price * 0.1; }
}
class SeniorDiscount implements DiscountPolicy { // new type added: no change to existing code!
public double calculate(double price) { return price * 0.15; }
}
class OrderService {
// Accepts any DiscountPolicy — closed for modification
double finalPrice(double price, DiscountPolicy policy) {
return price - policy.calculate(price);
}
}
L - Liskov Substitution Principle
"Subtypes must be substitutable for their base types."
Code using a parent type must work correctly when any child type is substituted in.
// ❌ LSP violation: Square inheriting Rectangle breaks behavior
class Rectangle {
protected int width, height;
void setWidth(int w) { this.width = w; }
void setHeight(int h) { this.height = h; }
int area() { return width * height; }
}
class Square extends Rectangle {
@Override void setWidth(int w) { this.width = this.height = w; } // sets both
@Override void setHeight(int h) { this.width = this.height = h; }
}
// Code expecting Rectangle, given a Square:
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(10);
System.out.println(r.area()); // Expected: 50, Actual: 100 ← LSP violation!
// ✅ LSP compliant: design with a common interface
interface Shape { int area(); }
class Rectangle implements Shape { /* ... */ }
class Square implements Shape { /* ... */ }
I - Interface Segregation Principle
"Clients should not be forced to depend on interfaces they do not use."
Many small, focused interfaces are better than one large interface.
// ❌ ISP violation: robots must implement eat() and sleep()
interface Worker {
void work();
void eat(); // robots don't eat...
void sleep(); // robots don't sleep...
}
class RobotWorker implements Worker {
public void work() { System.out.println("Robot working."); }
public void eat() { throw new UnsupportedOperationException("Robots don't eat"); }
public void sleep() { throw new UnsupportedOperationException("Robots don't sleep"); }
}
// ✅ ISP compliant: split interfaces by role
interface Workable { void work(); }
interface Eatable { void eat(); }
interface Sleepable { void sleep(); }
class HumanWorker implements Workable, Eatable, Sleepable {
public void work() { System.out.println("Human working."); }
public void eat() { System.out.println("Human eating."); }
public void sleep() { System.out.println("Human sleeping."); }
}
class RobotWorker implements Workable { // only what's needed
public void work() { System.out.println("Robot working."); }
}
D - Dependency Inversion Principle
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
Depend on interfaces (abstractions), not concrete implementation classes.
// ❌ DIP violation: high-level module directly depends on low-level implementation
class MySQLDatabase {
void save(String data) { System.out.println("Saving to MySQL: " + data); }
}
class UserService {
private MySQLDatabase db = new MySQLDatabase(); // directly depends on concrete class!
void saveUser(String user) { db.save(user); }
// Switching MySQL to PostgreSQL requires modifying UserService
}
// ✅ DIP compliant: depend on abstraction
interface Database {
void save(String data);
}
class MySQLDatabase implements Database {
public void save(String data) { System.out.println("MySQL: " + data); }
}
class PostgreSQLDatabase implements Database {
public void save(String data) { System.out.println("PostgreSQL: " + data); }
}
class UserService {
private final Database db; // depends on abstraction
UserService(Database db) { // constructor injection (DI)
this.db = db;
}
void saveUser(String user) { db.save(user); }
}
// Usage: inject the concrete implementation from outside
// (Spring does this automatically!)
UserService service1 = new UserService(new MySQLDatabase());
UserService service2 = new UserService(new PostgreSQLDatabase());
When to apply SOLID:
SOLID proves its worth as projects grow. Start simple, then refactor when you see these signals:
- SRP: A class has more than one reason to change → split it
- OCP: Adding new features requires editing existing classes → add abstractions
- LSP: Overridden methods throw exceptions or have empty implementations → reconsider inheritance
- ISP: Implementing classes littered with
throw new UnsupportedOperationException()→ split interfaces - DIP:
new ConcreteClass()scattered everywhere → use interfaces + dependency injection
In practice, Spring's @Autowired, @Service, and @Repository handle DIP and DI automatically.