Understanding SOLID Principles with Java
Tags: java, cleancode, solid, programming
Published: 2023-08-23 19:43:27

**Introduction:**

**SOLID** is an acronym that represents a set of five design principles for writing maintainable and scalable software. These principles were introduced by Robert C. Martin and have become fundamental concepts in object-oriented programming. In this blog article, we will explore each of the SOLID principles and provide Java code examples to illustrate their application.



## Single Responsibility Principle (SRP):

The SRP states that a class should have only one reason to change. In other words, a class should have only one responsibility. 

Let's consider an example where we have a Customer class that handles both customer data and customer-related calculations:

```java
public class Customer {
    private String name;
    private double balance;

    public void setName(String name) {
        this.name = name;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public double calculateDiscount() {
        // Calculate discount based on balance
        // ...
    }

    public void saveToDatabase() {
        // Save customer data to the database
        // ...
    }
}
```


In this case, the **Customer** class violates SRP. We can refactor it into two classes, **Customer** for data and **DiscountCalculator** for calculations, to adhere to SRP.

**Solution:**
Refactoring the **Customer** class into two separate classes, one for handling customer data and another for calculating discounts, is a great way to adhere to the Single Responsibility Principle (SRP). This separation of concerns makes your code more maintainable and easier to understand. It also allows for better code reuse, as the discount calculation logic can be used elsewhere in your application without duplicating code.

Here's a revised version of the code following this separation:

```java

public class Customer {
    private String name;
    private double balance;

    public Customer(String name, double balance) {
        this.name = name;
        this.balance = balance;
    }

    public String getName() {
        return name;
    }

    public double getBalance() {
        return balance;
    }
}
```

```java
public class DiscountCalculator {
    public double calculateDiscount(Customer customer) {
        if (customer.getBalance() >= 100.0) {
            return customer.getBalance() * 0.10;
        } 
        return 0.0;
    }
}
```
In this code, we have a **DiscountCalculator** class that calculates the discount based on the customer's balance. When you run the program with a customer whose balance is $120, it will calculate and display a $12 discount (which is 10% of $120).

```java
public class Main {
    public static void main(String[] args) {
        // Create a customer with a balance of $120
        Customer customer = new Customer("Alice", 120.0);

        // Create a DiscountCalculator
        DiscountCalculator calculator = new DiscountCalculator();

        // Calculate the discount for the customer
        double discount = calculator.calculateDiscount(customer);

        // Display the discount
        System.out.println("Discount for " + customer.getName() + ": $" + discount);
    }
}
```

## Open/Closed Principle (OCP):

The OCP states that software entities (classes, modules, functions) should be open for extension but closed for modification. This encourages the use of interfaces and abstract classes. 

Let's create a simple example:

```java
public interface Shape {
    double area();
}
```

```java
public class Rectangle implements Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }
}

```

```java
public class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

```
Here, we can easily add new shapes (e.g., Triangle) without modifying existing code.

## Liskov Substitution Principle (LSP):

The LSP states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. 

Let's demonstrate this with a Java example:

```java
public class Bird {
    void fly() {
        // Default fly behavior
    }
}

```

```java
public class Sparrow extends Bird {
    @Override
    void fly() {
        // Specific flying behavior for Sparrow
    }
}

```

```java
public class Ostrich extends Bird {
     // Ostrich cannot fly, so we don't override fly()
 
}
```
In this case, the **Ostrich** class violates the rule. We can refactor it. Here's a corrected example that better illustrates the principle:

```java
public interface Flyable {
    void fly();
}
```

```java
public class Bird {
    void move() {
        // Default move behavior
    }
}
```

```java
public class Sparrow extends Bird implements Flyable {
    @Override
    void move() {
        // Specific flying behavior for Sparrow
        System.out.println("Sparrow is flying.");
    }

    @Override
    public void fly() {
        move(); // Call the overridden move method to demonstrate flying
    }
}
```

```java
public class Ostrich extends Bird {
    @Override
    void move() {
        // Specific running behavior for Ostrich
        System.out.println("Ostrich is running.");
    }
}
```
```java
public class Main {
    public static void main(String[] args) {
        Sparrow sparrow = new Sparrow();
        Ostrich ostrich = new Ostrich();

        // Demonstrate flying for Sparrow
        sparrow.fly();

        // Demonstrate running for Ostrich
        ostrich.move();
    }
}

```
Now, we have a Flyable interface, and the Sparrow class implements it by providing the fly method. The Ostrich class, which can't fly, doesn't implement the Flyable interface.

In the Main class, you can see how to use the fly and move methods to demonstrate flying for sparrows and running for ostriches.

**Interface Segregation Principle (ISP):**

This principle suggests that clients (classes or modules) should not be forced to depend on interfaces they don't use. In other words, it promotes the idea of having smaller, more specific interfaces rather than large, monolithic ones.

Consider a scenario where we have an interface called Worker that represents workers who have both working and eating behaviors:

```java
public interface Worker {
    void work();
    void eat();
}
```
Now, let's say we have two classes, Engineer and Waiter, that implement this interface:

```java
public class Engineer implements Worker {
    @Override
    public void work() {
        // Engineer-specific work
    }

    @Override
    public void eat() {
        // Engineer-specific eating
    }
}

```

```java
public class Waiter implements Worker {
    @Override
    public void work() {
        // Waiter-specific work
    }

    @Override
    public void eat() {
        // Waiter-specific eating
    }
}
```
In this scenario, both Engineer and Waiter must implement both the work and eat methods, even though they may not use both behaviors. This violates the ISP because clients (in this case, the Engineer and Waiter classes) are forced to depend on methods they may not need.

To adhere to the ISP, we can create smaller, more specific interfaces for each behavior:

```java
public interface Workable {
    void work();
}
```

```java
public interface Eatable {
    void eat();
}
```
Now, our Engineer and Waiter classes can implement only the interfaces they need:

```java
public class Engineer implements Workable {
    @Override
    public void work() {
        // Engineer-specific work
    }
}
```

```java
public class Waiter implements Workable, Eatable {
    @Override
    public void work() {
        // Waiter-specific work
    }

    @Override
    public void eat() {
        // Waiter-specific eating
    }
}
```
By doing this, we adhere to the Interface Segregation Principle, and clients are not forced to implement methods they don't use. This results in more flexible and maintainable code.

## Dependency Inversion Principle (DIP):

The DIP states that high-level modules should not depend on low-level modules; both should depend on abstractions. Let's illustrate this with an example:

```java
public class LightBulb {
    void turnOn() {
        // Turn on the light bulb
    }
}
```

```java
public class Switch {
    private LightBulb bulb;

    public Switch(LightBulb bulb) {
        this.bulb = bulb;
    }

    void operate() {
        bulb.turnOn();
    }
}
```
In this case, Switch directly depends on LightBulb, violating DIP. We can introduce an abstraction to decouple them:

**Solution:**

```java
public interface Switchable {
    void turnOn();
}
```

```java
public class LightBulb implements Switchable {
    @Override
    public void turnOn() {
        // Turn on the light bulb
    }
}
```
```java
public class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    void operate() {
        device.turnOn();
    }
}
```
**Conclusion:**

Understanding and applying the SOLID principles in your Java code can lead to more maintainable, extensible, and robust software. By following these principles, you can improve code quality and make it easier to adapt to changing requirements. Remember that SOLID is not about following rules blindly but about guiding you to write better software.