In software engineering, writing code that is easy to maintain, extend, and understand is crucial. The SOLID principles are a set of five design guidelines that help developers achieve these goals by promoting clean, modular, and robust object-oriented design.
Let's explore each principle with clear Java examples.
1. Single Responsibility Principle (SRP)
Principle: A class should have only one reason to change, meaning it should have only one responsibility.
❌ Bad Example:
public class BankAccount {
private double balance;
public void deposit(double amount) { balance += amount; }
public void withdraw(double amount) { balance -= amount; }
// Violation: This class is also handling report generation
public void generateStatement() {
System.out.println("Statement generated for balance: " + balance);
}
}✅ Good Example:
public class BankAccount {
private double balance;
public void deposit(double amount) { balance += amount; }
public void withdraw(double amount) { balance -= amount; }
public double getBalance() { return balance; }
}
public class StatementService {
public void generateStatement(BankAccount account) {
System.out.println("Statement generated for balance: " + account.getBalance());
}
}Why it works: BankAccount manages account operations, while StatementService handles statements. Each class has a single, clear responsibility.
2. Open/Closed Principle (OCP)
Principle: Software entities should be open for extension but closed for modification.
❌ Bad Example:
public class InterestCalculator {
public double calculateInterest(String accountType, double balance) {
if (accountType.equals("SAVINGS")) {
return balance * 0.04;
} else if (accountType.equals("FIXED")) {
return balance * 0.06;
}
return 0;
}
}Adding a new account type requires modifying this class.
✅ Good Example:
public interface InterestPolicy {
double calculateInterest(double balance);
}
public class SavingsAccountInterest implements InterestPolicy {
public double calculateInterest(double balance) { return balance * 0.04; }
}
public class FixedDepositInterest implements InterestPolicy {
public double calculateInterest(double balance) { return balance * 0.06; }
}Why it works: New account types can be added by creating new classes without altering existing code.
3. Liskov Substitution Principle (LSP)
Principle: Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
❌ Bad Example:
public class Account {
public void withdraw(double amount) {
System.out.println("Withdrawn: " + amount);
}
}
public class FixedDepositAccount extends Account {
@Override
public void withdraw(double amount) {
throw new UnsupportedOperationException("Cannot withdraw from Fixed Deposit!");
}
}Using FixedDepositAccount in place of Account breaks the program.
✅ Good Example:
public abstract class Account {
protected double balance;
public abstract double getBalance();
}
public class SavingsAccount extends Account {
public void withdraw(double amount) { balance -= amount; }
public double getBalance() { return balance; }
}
public class FixedDepositAccount extends Account {
public double getBalance() { return balance; }
}Why it works: Both subclasses can be used interchangeably via the Account abstraction without unexpected errors.
4. Interface Segregation Principle (ISP)
Principle: Clients should not be forced to depend on interfaces they do not use.
❌ Bad Example:
public interface BankingOperations {
void deposit(double amount);
void withdraw(double amount);
void applyForLoan(double amount);
}
public class LoanAccount implements BankingOperations {
// Forced to implement irrelevant methods
public void deposit(double amount) { /* Not applicable */ }
public void withdraw(double amount) { /* Not applicable */ }
public void applyForLoan(double amount) { /* logic */ }
}✅ Good Example:
public interface DepositOperation {
void deposit(double amount);
}
public interface WithdrawOperation {
void withdraw(double amount);
}
public interface LoanOperation {
void applyForLoan(double amount);
}
public class SavingsAccount implements DepositOperation, WithdrawOperation {
public void deposit(double amount) { /* logic */ }
public void withdraw(double amount) { /* logic */ }
}
public class LoanAccount implements LoanOperation {
public void applyForLoan(double amount) { /* logic */ }
}Why it works: Interfaces are small and specific, so classes only implement what they need.
5. Dependency Inversion Principle (DIP)
Principle: Depend on abstractions, not on concrete implementations.
❌ Bad Example:
@RestController
public class AccountController {
// Tightly coupled to a concrete class
private RegularAccountService accountService = new RegularAccountService();
@GetMapping("/{id}")
public Account getAccount(@PathVariable Long id) {
return accountService.getAccountById(id);
}
}✅ Good Example:
public interface AccountService {
Account getAccountById(Long id);
}
@Service
public class RegularAccountService implements AccountService {
public Account getAccountById(Long id) { /* logic */ }
}
@RestController
public class AccountController {
private final AccountService accountService;
// Dependency is injected via interface
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
}Why it works: The controller depends on the AccountService interface, making it easy to switch implementations without code changes.
Conclusion
The SOLID principles are foundational to writing clean, scalable, and maintainable object-oriented code. By applying:
- SRP to keep classes focused,
- OCP to enable easy extension,
- LSP to ensure reliable inheritance,
- ISP to create lean interfaces, and
- DIP to decouple dependencies,
you can significantly improve the quality and longevity of your software systems.