SOLID Principles Explained (with examples in Java)
Tags: java, computerscience, solidprinciples, programming
Published: 2024-05-02 15:41:06

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.

```java
// 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.

``` java
// 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.

```java
// 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.

```java
// 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.

```java
// 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.

```java
// 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.

```java
// 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.

```java
// 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.