The SOLID principles, conceptualized by Robert C. Martin (aka Uncle Bob), is a fundamental design principles that aim to create well-structured and maintainable code. This article will walk you through the 5 principles with examples.
1. Single Responsibility Principle (SRP)
- "A class should have only one reason to change."
- Each class, method, or function should serve a single, well-defined purpose, with all elements within it supporting that purpose.
- If a change needs to be made, it should only affect that single responsibility, and not other unrelated parts of the codebase.
// Violates SRP
public class BankAccount {
private double balance;
public void deposit(double amount) {
balance += amount;
}
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
} else {
System.out.println("Insufficient funds");
}
}
public void printBalance() {
System.out.println("Current balance: " + balance);
}
}
The code above violates SRP because say if the requirement changed to display the balance in a different format, then the BankAccount class would need to be updated, hence violating SRP. To resolve this, we can separate them into 2 classes, ensuring that each class has a single responsibility.
// Follows SRP
public class BankAccount {
private double balance;
public void deposit(double amount) {
balance += amount;
}
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
} else {
throw new IllegalArgumentException("Insufficient funds");
}
}
public double getBalance() {
return balance;
}
}
public class BalanceDisplayer {
public void printBalance(BankAccount account) {
System.out.println("Current balance: " + account.getBalance());
}
}
2. Open closed Principle (OSP)
- "Software components should be open for extension but closed for modification."
- New functionality can be added without changing existing code.
Using the same example above, let's say we want to display the balance in a few formats. To modify the BalanceDisplayer class without violating OCP, we need to design the code in such a way that everyone can reuse the feature by just extending it.
// Interface for balance display
public interface BalanceDisplay {
void displayBalance(BankAccount account);
}
// Implementation for displaying balance in a simple format
public class SimpleBalanceDisplay implements BalanceDisplay {
@Override
public void displayBalance(BankAccount account) {
System.out.println("Current balance: " + account.getBalance());
}
}
// Implementation for displaying balance in a fancy format
public class FancyBalanceDisplay implements BalanceDisplay {
@Override
public void displayBalance(BankAccount account) {
System.out.println("~~~ Fancy Balance: $" + account.getBalance() + " ~~~");
}
3. Liskov substitution Principle (LSP)
- "Derived or child classes must be substitutable for their base or parent classes."
- If B is a subclass of A, B should be able to replace A without affecting the correctness of the program.
// Subclass SavingsAccount
public class SavingsAccount extends BankAccount {
public SavingsAccount(double balance) {
super(balance);
}
// Additional functionality specific to SavingsAccount
public void calculateInterest() {
// Calculate interest for savings account
}
}
public class GoldAccount extends BankAccount {
private double bonusPoints;
public GoldAccount(double balance, double bonusPoints) {
super(balance);
this.bonusPoints = bonusPoints;
}
@Override
public void deposit(double amount) {
balance += amount + (bonusPoints * 0.1); // Adds bonus points to the deposit
}
}
- SavingsAccount adheres to LSP as it extends the functionality by adding specific methods like calculateInterest, which do not alter the core behavior of depositing and withdrawing funds.
- GoldAccount class violates LSP by changing the behavior of the deposit method from the base BankAccount class.
4. Interface Segregation Principle (ISP)
- "Do not force any client to implement an interface which is irrelevant to them."
- Clients should not be compelled to implement interfaces that contain methods they do not use.
- Instead of having a single large interface, it is better to have multiple smaller interfaces, each focusing on a specific set of methods relevant to a particular functionality.
// Violation of ISP
public interface IBankAccount {
void deposit(double amount);
void withdraw(double amount);
double getBalance();
void printStatement();
void requestLoan();
}
public class BankAccount implements IBankAccount {
private double balance;
public void deposit(double amount) {
// implementation details
}
public void withdraw(double amount) {
// implementation details
}
public double getBalance() {
// implementation details
}
public void printStatement() {
// implementation details
}
public void requestLoan() {
// implementation details
}
}
- The IBankAccount interface violates ISP by including methods that are not relevant to all classes that implement it.
- The BankAccount class implements the entire IBankAccount interface, even though it may not need the requestLoan method.
// Adheres to ISP
// Interface for account management
public interface AccountManager {
void deposit(double amount);
void withdraw(double amount);
double getBalance();
}
// Interface for account reporting
public interface AccountReporter {
void printStatement();
}
// Interface for loan management
public interface LoanManager {
void requestLoan();
}
Each class now depends only on the interfaces relevant to its responsibilities, adhering to ISP.
5. Dependency Inversion Principle (DIP)
- "High-level modules should not depend on low-level modules. Both should depend on abstractions."
- "Abstractions should not depend on details. Details should depend on abstractions."
- Think of it like a restaurant. The high-level module is the restaurant, and the low-level module is the kitchen. The restaurant should not directly depend on the kitchen. Instead, both should depend on a common language, like English. The kitchen should not depend on the restaurant's specific menu. Instead, the menu should depend on the kitchen's cooking skills.
// Violates DIP
public class ShoppingMall {
private BankAccount bankAccount;
public ShoppingMall(BankAccount bankAccount) {
this.bankAccount = bankAccount;
}
public void doPayment(String order, double amount) {
bankAccount.withdraw(amount);
// Process the payment
}
}
In this example, the ShoppingMall class directly depends on the BankAccount class, which violates DIP. The ShoppingMall class is a high-level module, and the BankAccount class is a low-level module.
To fix this, we can introduce an abstraction that both the ShoppingMall and BankAccount classes can depend on.
// Adhering to DIP
public interface PaymentProcessor {
void processPayment(double amount);
double getBalance();
}
public class BankAccount implements PaymentProcessor {
// implementation details
}
public class ShoppingMall {
private PaymentProcessor paymentProcessor;
public ShoppingMall(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public void doPayment(String order, double amount) {
paymentProcessor.processPayment(amount);
// Process the payment
}
}
Thank you for reading this article. I hope you found it informative and that it helps you write better, more robust code in your future projects.