Mastering SOLID Principles in Java: A Practical Guide
Tags: java, solidprinciples
Published: 2024-06-26 14:39:23

SOLID principles are fundamental for any developer aiming to build robust, maintainable systems. These principles not only enhance code quality but also facilitate teamwork and scalability of projects. Let’s delve into each of these principles with practical examples in Java, highlighting both common violations and recommended practices.

### 1. Single Responsibility Principle (SRP)
Principle: A class should have only one reason to change.

Violating the SRP:

```java
public class User {
    private String name;
    private String email;

    public void saveUser() {
        // Logic to save the user in the database
    }

    public void sendEmail() {
        // Logic to send an email to the user
    }
}
```

In this example, the `User` class has more than one responsibility: managing user data and sending emails.

Applying the SRP:

```java
public class User {
    private String name;
    private String email;
}

public class UserRepository {
    public void saveUser(User user) {
        // Logic to save the user in the database
    }
}

public class EmailService {
    public void sendEmail(User user) {
        // Logic to send an email to the user
    }
}
```
Here, we have separated the responsibilities into different classes, adhering to the SRP.

###2. Open/Closed Principle (OCP)
Principle: Classes should be open for extension, but closed for modification.

Violating the OCP:

```java
public class DiscountCalculator {
    public double calculateDiscount(String type) {
        if (type.equals("NORMAL")) {
            return 0.05;
        } else if (type.equals("SPECIAL")) {
            return 0.1;
        }
        return 0;
    }
}
```
In this example, any new discount type would require modifying the `DiscountCalculator` class.

Applying the OCP:

```java
public interface Discount {
    double calculateDiscount();
}

public class NormalDiscount implements Discount {
    public double calculateDiscount() {
        return 0.05;
    }
}

public class SpecialDiscount implements Discount {
    public double calculateDiscount() {
        return 0.1;
    }
}

public class DiscountCalculator {
    public double calculateDiscount(Discount discount) {
        return discount.calculateDiscount();
    }
}
```
In this case, the `DiscountCalculator` is closed for modification but open for extension through the implementation of new discount types.

###3. Liskov Substitution Principle (LSP)
Principle: Subclasses should be replaceable by their base classes without affecting the correctness of the program.

Violating the LSP:

```java
public class Bird {
    public void fly() {
        // flying implementation
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly");
    }
}
```
Here, the `Penguin` class cannot replace `Bird` without affecting the program’s correctness.

Applying the LSP:

```java
public abstract class Bird {
}

public class FlyingBird extends Bird {
    public void fly() {
        // flying implementation
    }
}

public class Penguin extends Bird {
}
```
Now, `FlyingBird` and Penguin are separated, respecting the ability of `Bird` to be replaced by its subclasses.

###4. Interface Segregation Principle (ISP)
Principle: Clients should not be forced to depend on interfaces they do not use.

Violating the ISP:

```java
public interface Animal {
    void walk();
    void fly();
    void swim();
}

public class Dog implements Animal {
    public void walk() {
        // walking implementation
    }

    public void fly() {
        throw new UnsupportedOperationException();
    }

    public void swim() {
        // dogs can swim
    }
}
```
Here, `Dog` is forced to implement `fly`, which is not relevant.

Applying the ISP:

```java
public interface Walkable {
    void walk();
}

public interface Flyable {
    void fly();
}

public interface Swimmable {
    void swim();
}

public class Dog implements Walkable, Swimmable {
    public void walk() {
        // walking implementation
    }

    public void swim() {
        // swimming implementation
    }
}
```
Now, `Dog` implements only the interfaces relevant to its actions.

###5. Dependency Inversion Principle (DIP)
Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Violating the DIP:

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

public class ElectricPowerSwitch {
    private LightBulb lightBulb = new LightBulb();

    public void press() {
        lightBulb.turnOn();
    }
}
```
`ElectricPowerSwitch` directly depends on `LightBulb`, a low-level module.

Applying the DIP:

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

public class LightBulb implements Switchable {
    public void turnOn() {
        // turn on the light bulb
    }
}

public class ElectricPowerSwitch {
    private Switchable client;

    public ElectricPowerSwitch(Switchable client) {
        this.client = client;
    }

    public void press() {
        client.turnOn();
    }
}
```
Now, `ElectricPowerSwitch` depends on an abstraction (`Switchable`), which makes the design more flexible and sustainable.

Conclusion
Applying the SOLID principles in Java is not just good theoretical practice but a proven strategy for keeping software flexible, sustainable, and comprehensible. I hope these examples help illustrate how you can implement these principles in your own software projects.