Understanding SOLID Principles with Java Examples

Understanding SOLID Principles with Java Examples

Introduction: SOLID principles are essential for building robust applications. Coding with these principles not only makes your code more readable but also significantly enhances its maintainability, making it easier to scale and modify over time.

Let’s dive deeper with practical examples to see how each principle can be applied in real-world Java scenarios

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should only have one job or responsibility.

Example:

Consider a class that handles both user management and report generation. This violates SRP because it has more than one responsibility.

Before SRP:

public class UserService {
    public void addUser(User user) {
        // logic to add user
    }

    public void generateUserReport(User user) {
        // logic to generate report
    }
}        

After SRP:

public class UserService {
    public void addUser(User user) {
        // logic to add user
    }
}

public class ReportService {
    public void generateUserReport(User user) {
        // logic to generate report
    }
}        

2. Open-Closed Principle (OCP)

Definition: Software entities should be open for extension, but closed for modification.

Example:

Let's say you have a payment processing system where initially, only credit card payments are supported. If we later add PicPay payments, we shouldn't modify the existing code for credit card processing but instead extend it.

Before OCP:

public class PaymentProcessor {
    public void processPayment(String paymentType) {
        if (paymentType.equals("creditCard")) {
            // process credit card
        } else if (paymentType.equals("picpay")) {
            // process PicPay
        }
    }
}        

After OCP:

public interface Payment {
    void processPayment();
}

public class CreditCardPayment implements Payment {
    public void processPayment() {
        // process credit card
    }
}

public class PicPayPayment implements Payment {
    public void processPayment() {
        // process PicPay
    }
}

public class PaymentProcessor {
    public void processPayment(Payment payment) {
        payment.processPayment();
    }
}
        

3. Liskov Substitution Principle (LSP)

Definition: Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

Example:

If we have a Bird superclass and a Penguin subclass, LSP states that we should be able to substitute Bird with Penguin without breaking the behavior of the program. But since penguins cannot fly, it violates LSP if the subclass changes expected behaviors.

Before LSP:

public class Bird {
    public void fly() {
        // fly logic
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins cannot fly");
    }
}        

After LSP:

public interface Flyable {
    void fly();
}

public class Bird {
    // common bird properties
}

public class Sparrow extends Bird implements Flyable {
    @Override
    public void fly() {
        // fly logic
    }
}

public class Penguin extends Bird {
    // no fly method
}        

4. Interface Segregation Principle (ISP)

Definition: A client should not be forced to implement interfaces they don’t use.

Example:

If we have an interface that enforces all birds to fly and swim, it would force classes like Sparrow (which doesn't swim) to implement methods it doesn't need.

Before ISP:

public interface Bird {
    void fly();
    void swim();
}

public class Sparrow implements Bird {
    @Override
    public void fly() {
        // fly logic
    }

    @Override
    public void swim() {
        // unnecessary for Sparrow
    }
}        

After ISP:

public interface Flyable {
    void fly();
}

public interface Swimmable {
    void swim();
}

public class Sparrow implements Flyable {
    @Override
    public void fly() {
        // fly logic
    }
}

public class Penguin implements Swimmable {
    @Override
    public void swim() {
        // swim logic
    }
}        

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Example:

A class depending directly on another class (low-level dependency) violates DIP. It should depend on an abstraction instead.

Before DIP:

public class Light {
    public void turnOn() {
        System.out.println("Light on");
    }
}

public class Switch {
    private final Light light;

    public Switch(Light light) {
        this.light = light;
    }

    public void toggle() {
        light.turnOn();
    }
}        

After DIP:

public interface Switchable {
    void turnOn();
}

public class Light implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("Light on");
    }
}

public class Switch {
    private final Switchable device;

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

    public void toggle() {
        device.turnOn();
    }
}        

Now, Switch depends on the Switchable interface, adhering to DIP.

Conclusion: By applying SOLID principles, developers can create cleaner, more maintainable, and scalable applications. These principles guide us in designing robust systems that are easy to extend and adapt over time without introducing unnecessary complexity or breaking existing functionality. Start implementing SOLID in your projects today, and you'll notice how much easier it becomes to manage and grow your codebase.

Hope, that you understood a little bit more of SOLID principles and how to use.

Guilherme Carvalho

Full Stack Developer | Java, Spring Boot | C#, .NET | JavaScript, Node.js, Angular | ServiceNow Application Developer

7mo

Hi Carlos, how have you been? Great article on the SOLID principles! It's true that many of us don't always realize how much these principles can transform our code for the better. Personally, the principle I like the most is the Open/Closed Principle. It helps prevent us from breaking our code as it grows. One of the biggest headaches for a programmer is dealing with a large codebase. When you try to add new features, you often end up accidentally disrupting everything. The Open/Closed Principle allows us to keep classes open for extension but closed for modification, making the process much smoother. Dependency Inversion is also crucial, as it helps reduce coupling between classes. In general, combining SOLID principles with design patterns is one of the best ways to achieve high-performance, maintainable code. Excellent work on the article!

Like
Reply
Leandro Veiga

Senior Software Engineer | Full Stack Developer | C# | .NET | .NET Core | React | Amazon Web Service (AWS)

7mo

Great explanation Carlos Eduardo Junior! SOLID principles are essential for writing clean and scalable code, and applying them consistently makes a big difference in long-term maintainability.

Like
Reply
Idalio Pessoa

Senior Ux Designer | Product Designer | UX/UI Designer | UI/UX Designer | Figma | Design System |

7mo

Applying SOLID principles to coding is like designing user interfaces - it's all about creating a robust foundation for future growth and changes.

Like
Reply

Good explanation with the examples , thanks for sharing.

Like
Reply
Lucas Wolff

.NET Developer | C# | TDD | Angular | Azure | SQL

7mo

Very insightful!

Like
Reply

To view or add a comment, sign in

More articles by Carlos Eduardo Junior

Insights from the community

Others also viewed

Explore topics