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.
- Single
Responsibility Principle (SRP)
- Open/Closed
Principle (OCP)
- Liskov
Substitution Principle (LSP)
- Interface
Segregation Principle (ISP)
- Dependency
Inversion Principle (DIP)
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.
Here’s a base class:
public class BankAccount {
protected double
balance;
public
BankAccount(double balance) {
this.balance =
balance;
}
public void
deposit(double amount) {
balance +=
amount;
}
public void
withdraw(double amount) {
balance -=
amount;
}
public double
getBalance() {
return
balance;
}
}
Now, here's a subclass that respects LSP:
public class SavingsAccount extends BankAccount {
public
SavingsAccount(double balance) {
super(balance);
}
// Adds new
functionality, doesn't change base behavior
public void
calculateInterest() {
double
interest = balance * 0.03; // 3% interest
balance +=
interest;
}
}
You can replace BankAccount with SavingsAccount without
breaking any base functionality like deposit, withdraw, or getBalance.
LSP-Violating Example: GoldAccount
This subclass changes core behavior (deposit):
public class GoldAccount extends BankAccount {
private double
bonusPoints;
public
GoldAccount(double balance, double bonusPoints) {
super(balance);
this.bonusPoints = bonusPoints;
}
// Modifies base
behavior: adds bonus points to deposit
@Override
public void
deposit(double amount) {
balance +=
amount + (bonusPoints * 0.1);
}
}
This violates LSP because if you write code assuming the
behavior of BankAccount.deposit() (which adds exactly the amount), and you
substitute it with GoldAccount, the logic now behaves differently.
public class BankService {
public static void
makeDeposit(BankAccount account, double amount) {
account.deposit(amount);
System.out.println("Balance after deposit: " +
account.getBalance());
}
}
If we call:
BankAccount account = new GoldAccount(1000, 50);
BankService.makeDeposit(account, 100);
Expected balance: 1000 + 100 = 1100
Actual balance: 1000 + 100 + (50 * 0.1) = 1105
- 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
// Low-level class
public class BankAccount {
private double
balance;
public BankAccount(double
initialBalance) {
this.balance =
initialBalance;
}
public void withdraw(double
amount) {
balance -=
amount;
System.out.println("Withdrawn: " + amount + ", Remaining
balance: " + balance);
}
public double getBalance()
{
return
balance;
}
}
// High-level class depending directly on low-level class
public class ShoppingMall {
private
BankAccount bankAccount;
public ShoppingMall(BankAccount
bankAccount) {
this.bankAccount = bankAccount;
}
public void doPayment(String
order, double amount) {
System.out.println("Processing order: " + order);
bankAccount.withdraw(amount); // tightly coupled
}
}
// Main class
public class Main {
public static
void main(String[] args) {
BankAccount
bankAccount = new BankAccount(1000);
ShoppingMall
mall = new ShoppingMall(bankAccount);
mall.doPayment("Shoes", 200);
}
}
Problem:
- ShoppingMall
is directly coupled to BankAccount.
- If you
want to switch to another payment method (e.g., credit card, PayPal),
you'll need to change ShoppingMall, which violates DIP.
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
// Abstraction
public interface PaymentProcessor {
void
processPayment(double amount);
double
getBalance();
}
// Low-level module implementing abstraction
public class BankAccount implements PaymentProcessor {
private double
balance;
public
BankAccount(double initialBalance) {
this.balance =
initialBalance;
}
@Override
public void processPayment(double
amount) {
balance -=
amount;
System.out.println("BankAccount payment processed. Amount: " +
amount + ", Remaining balance: " + balance);
}
@Override
public double getBalance()
{
return
balance;
}
}
// High-level module depending on abstraction
public class ShoppingMall {
private
PaymentProcessor paymentProcessor;
public ShoppingMall(PaymentProcessor
paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public void doPayment(String
order, double amount) {
System.out.println("Processing order: " + order);
paymentProcessor.processPayment(amount);
}
}
// Main.java
public class Main {
public static void
main(String[] args) {
PaymentProcessor bankAccount = new BankAccount(1000); // Abstraction
ShoppingMall
mall = new ShoppingMall(bankAccount);
// Uses abstraction
mall.doPayment("Laptop", 300);
}
}
Benefits of This Approach:
- ShoppingMall
is now independent of specific payment implementations.
- You
can easily add new payment methods like CreditCard, PayPalAccount,
etc., by just implementing PaymentProcessor, without modifying ShoppingMall.
Add Another Payment Method (Optional Extension)
public class PayPalAccount implements PaymentProcessor {
private double
balance;
public
PayPalAccount(double balance) {
this.balance =
balance;
}
@Override
public void
processPayment(double amount) {
balance -=
amount;
System.out.println("PayPal payment processed. Amount: " +
amount + ", Remaining balance: " + balance);
}
@Override
public double
getBalance() {
return
balance;
}
}
PaymentProcessor paypal = new PayPalAccount(500);
ShoppingMall mall2 = new ShoppingMall(paypal);
mall2.doPayment("Online Course", 150);
No comments:
Post a Comment