Monday, 16 June 2025

 

                                                                  SOLID principles IN JAVA

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)
  2. Open/Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. 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