DEV Community

Cover image for Mastering SOLID Principles in Java: A Practical Guide

Mastering SOLID Principles in Java: A Practical Guide

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:

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

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

Applying the SRP:

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

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:

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

In this example, any new discount type would require modifying the DiscountCalculator class.

Applying the OCP:

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

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:

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

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly");
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, the Penguin class cannot replace Bird without affecting the program’s correctness.

Applying the LSP:

public abstract class Bird {
}

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

public class Penguin extends Bird {
}
Enter fullscreen mode Exit fullscreen mode

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:

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

Here, Dog is forced to implement fly, which is not relevant.

Applying the ISP:

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

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:

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

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

    public void press() {
        lightBulb.turnOn();
    }
}
Enter fullscreen mode Exit fullscreen mode

ElectricPowerSwitch directly depends on LightBulb, a low-level module.

Applying the DIP:

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

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.

Top comments (0)