DEV Community

Cover image for SOLID Principles Explained (with examples in Java)
rachel
rachel

Posted on

SOLID Principles Explained (with examples 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)

  • "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);
    }
}

Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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() + " ~~~");
    }
Enter fullscreen mode Exit fullscreen mode

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
    }
}

Enter fullscreen mode Exit fullscreen mode
  • 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
    }
}

Enter fullscreen mode Exit fullscreen mode
  • 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();
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
springstudent profile image
Baby Is Daddy's Master

The example of the code you've given is very good.