The SOLID principles are five core design principles in object-oriented programming (OOP) that help create code that is:
-
Easy to understand
-
Flexible to change
-
Maintainable over time
They were popularized by Robert C. Martin (Uncle Bob) and are very relevant in Java development.
Let’s go through each principle with a short explanation and a Java-based example:
✅ S – Single Responsibility Principle (SRP)
A class should have only one reason to change.
🔸 Explanation:
Each class should do one thing only and do it well. This makes the class easier to maintain and test.
💡 Java Example:
// ❌ Violates SRP – doing both user logic and saving
class User {
void register(String username, String password) {
// registration logic
}
void saveToDatabase() {
// DB saving logic
}
}
// ✅ Follows SRP – separate responsibilities
class User {
String username;
String password;
// getters/setters
}
class UserService {
void register(User user) {
// registration logic
}
}
class UserRepository {
void save(User user) {
// DB saving logic
}
}
✅ O – Open/Closed Principle (OCP)
Software entities should be open for extension, but closed for modification.
🔸 Explanation:
You should be able to add new functionality without modifying existing code, often using abstraction.
💡 Java Example:
// ❌ Violates OCP – modifying method every time a new shape is added
class AreaCalculator {
double calculateArea(Object shape) {
if (shape instanceof Circle) {
return Math.PI * ((Circle) shape).radius * ((Circle) shape).radius;
} else if (shape instanceof Square) {
return ((Square) shape).side * ((Square) shape).side;
}
return 0;
}
}
// ✅ Follows OCP – open for extension via polymorphism
interface Shape {
double area();
}
class Circle implements Shape {
double radius;
public double area() {
return Math.PI * radius * radius;
}
}
class Square implements Shape {
double side;
public double area() {
return side * side;
}
}
class AreaCalculator {
double calculateArea(Shape shape) {
return shape.area();
}
}
✅ L – Liskov Substitution Principle (LSP)
Subclasses should be substitutable for their base classes without breaking the behavior.
🔸 Explanation:
A subclass should honor the contract of the base class. You should be able to use a subclass wherever the parent class is expected.
💡 Java Example:
class Bird {
void fly() {
System.out.println("Bird is flying");
}
}
class Sparrow extends Bird {}
class Ostrich extends Bird {
@Override
void fly() {
throw new UnsupportedOperationException("Ostrich can't fly!");
}
}
// ❌ Violates LSP – Ostrich breaks the expected behavior of Bird
// ✅ Better approach
interface Bird {}
interface FlyingBird extends Bird {
void fly();
}
class Sparrow implements FlyingBird {
public void fly() {
System.out.println("Sparrow is flying");
}
}
class Ostrich implements Bird {
// no fly method – behavior preserved
}
✅ I – Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
🔸 Explanation:
Avoid fat interfaces. Instead, split them into smaller, more specific ones.
💡 Java Example:
// ❌ Violates ISP – PrinterScannerCopier forces all classes to implement unused methods
interface PrinterScannerCopier {
void print();
void scan();
void copy();
}
class SimplePrinter implements PrinterScannerCopier {
public void print() { }
public void scan() { throw new UnsupportedOperationException(); }
public void copy() { throw new UnsupportedOperationException(); }
}
// ✅ Follows ISP – smaller interfaces
interface Printer {
void print();
}
interface Scanner {
void scan();
}
class SimplePrinter2 implements Printer {
public void print() { }
}
✅ D – Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
🔸 Explanation:
Depend on interfaces, not concrete implementations. It helps with loose coupling.
💡 Java Example:
// ❌ Violates DIP – tightly coupled to EmailService
class EmailService {
void send(String message) { }
}
class Notification {
private EmailService emailService = new EmailService();
void sendAlert(String msg) {
emailService.send(msg);
}
}
// ✅ Follows DIP – depends on abstraction
interface MessageService {
void send(String message);
}
class EmailService implements MessageService {
public void send(String message) { }
}
class Notification {
private MessageService service;
public Notification(MessageService service) {
this.service = service;
}
void sendAlert(String msg) {
service.send(msg);
}
}
🧠 Summary Table:
Principle | Responsibility |
---|---|
S | One reason to change per class |
O | Extend, don’t modify |
L | Subclasses must honor base behavior |
I | Prefer small, focused interfaces |
D | Rely on abstractions, not concrete classes |