What is SOLID?
SOLID is an acronym for five design principles that help developers create maintainable, flexible, and understandable object-oriented software.
- S - Single Responsibility Principle
- O - Open/Closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D - Dependency Inversion Principle
S - Single Responsibility Principle
"A class should have only one reason to change."
Each class should focus on doing one thing well. If a class has multiple responsibilities, changes to one aspect might break another.
Violation Example
// BAD: This class does too many things
public class UserManager {
public void createUser(User user) {
// Validate user
if (user.getEmail() == null) {
throw new ValidationException("Email required");
}
// Save to database
Connection conn = DriverManager.getConnection(url);
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.executeUpdate();
// Send email
Properties props = new Properties();
Session session = Session.getInstance(props);
Transport.send(message);
// Log the action
FileWriter fw = new FileWriter("log.txt");
fw.write("User created: " + user.getId());
}
}
Correct Implementation
// GOOD: Each class has one responsibility
public class UserValidator {
public void validate(User user) {
if (user.getEmail() == null) {
throw new ValidationException("Email required");
}
}
}
public class UserRepository {
public void save(User user) {
// Database operations only
}
}
public class EmailService {
public void sendWelcomeEmail(User user) {
// Email operations only
}
}
public class UserService {
private final UserValidator validator;
private final UserRepository repository;
private final EmailService emailService;
public void createUser(User user) {
validator.validate(user);
repository.save(user);
emailService.sendWelcomeEmail(user);
}
}
O - Open/Closed Principle
"Software entities should be open for extension, but closed for modification."
You should be able to add new functionality without changing existing code.
Violation Example
// BAD: Must modify this class to add new payment types
public class PaymentProcessor {
public void processPayment(String type, double amount) {
if (type.equals("credit_card")) {
// Process credit card
} else if (type.equals("paypal")) {
// Process PayPal
} else if (type.equals("crypto")) {
// Adding new type requires modifying this class!
}
}
}
Correct Implementation
// GOOD: Open for extension via new implementations
public interface PaymentMethod {
void process(double amount);
}
public class CreditCardPayment implements PaymentMethod {
@Override
public void process(double amount) {
// Credit card logic
}
}
public class PayPalPayment implements PaymentMethod {
@Override
public void process(double amount) {
// PayPal logic
}
}
// Adding crypto is just a new class - no modification needed!
public class CryptoPayment implements PaymentMethod {
@Override
public void process(double amount) {
// Crypto logic
}
}
// Processor is closed for modification
public class PaymentProcessor {
public void processPayment(PaymentMethod method, double amount) {
method.process(amount);
}
}
L - Liskov Substitution Principle
"Objects of a superclass should be replaceable with objects of its subclasses without breaking the program."
Subclasses must be usable through the base class interface without the need to know the difference.
Violation Example
// BAD: Square violates LSP when used as Rectangle
public class Rectangle {
protected int width;
protected int 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; // Breaks expected behavior!
}
@Override
public void setHeight(int height) {
this.height = height;
this.width = height; // Breaks expected behavior!
}
}
// This test fails with Square!
void testRectangle(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert r.getArea() == 20; // Fails for Square (returns 16)
}
Correct Implementation
// GOOD: Use interface instead of inheritance
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
private final int width;
private final int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
public class Square implements Shape {
private final int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
I - Interface Segregation Principle
"Clients should not be forced to depend on interfaces they don't use."
Create smaller, focused interfaces rather than large, general-purpose ones.
Violation Example
// BAD: Fat interface forces implementations to include unused methods
public interface Worker {
void work();
void eat();
void sleep();
}
// Robot doesn't eat or sleep!
public class Robot implements Worker {
@Override
public void work() {
// Robot works
}
@Override
public void eat() {
// Forced to implement - throws exception or does nothing
throw new UnsupportedOperationException();
}
@Override
public void sleep() {
throw new UnsupportedOperationException();
}
}
Correct Implementation
// GOOD: Segregated interfaces
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
// Human implements all
public class Human implements Workable, Eatable, Sleepable {
@Override
public void work() { }
@Override
public void eat() { }
@Override
public void sleep() { }
}
// Robot only implements what it needs
public class Robot implements Workable {
@Override
public void work() {
// Robot works
}
}
D - Dependency Inversion Principle
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
Depend on interfaces rather than concrete implementations.
Violation Example
// BAD: High-level class depends on concrete low-level class
public class MySQLDatabase {
public void save(String data) {
// MySQL-specific code
}
}
public class UserService {
private MySQLDatabase database = new MySQLDatabase(); // Tight coupling!
public void saveUser(User user) {
database.save(user.toString());
// Can't easily switch to PostgreSQL or test with mock
}
}
Correct Implementation
// GOOD: Both depend on abstraction
public interface Database {
void save(String data);
}
public class MySQLDatabase implements Database {
@Override
public void save(String data) {
// MySQL implementation
}
}
public class PostgreSQLDatabase implements Database {
@Override
public void save(String data) {
// PostgreSQL implementation
}
}
public class UserService {
private final Database database; // Depends on abstraction
// Dependency injected
public UserService(Database database) {
this.database = database;
}
public void saveUser(User user) {
database.save(user.toString());
}
}
// Usage - can swap implementations easily
UserService service1 = new UserService(new MySQLDatabase());
UserService service2 = new UserService(new PostgreSQLDatabase());
// Testing - can use mock
Database mockDb = mock(Database.class);
UserService testService = new UserService(mockDb);
SOLID Summary
| Principle | Key Idea | Benefit |
|---|---|---|
| SRP | One reason to change | Easier maintenance |
| OCP | Extend, don't modify | Safer additions |
| LSP | Substitutable subclasses | Reliable polymorphism |
| ISP | Small, focused interfaces | Flexible implementations |
| DIP | Depend on abstractions | Testable, decoupled code |