DEV Community

Diallo Mamadou Pathé
Diallo Mamadou Pathé

Posted on • Updated on

Understanding SOLID Principles with Java

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:

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

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:


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;
    }
}
Enter fullscreen mode Exit fullscreen mode
public class DiscountCalculator {
    public double calculateDiscount(Customer customer) {
        if (customer.getBalance() >= 100.0) {
            return customer.getBalance() * 0.10;
        } 
        return 0.0;
    }
}
Enter fullscreen mode Exit fullscreen mode

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).

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

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:

public interface Shape {
    double area();
}
Enter fullscreen mode Exit fullscreen mode
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;
    }
}

Enter fullscreen mode Exit fullscreen mode
public class Circle implements Shape {
    private double radius;

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

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

Enter fullscreen mode Exit fullscreen mode

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:

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

Enter fullscreen mode Exit fullscreen mode
public class Sparrow extends Bird {
    @Override
    void fly() {
        // Specific flying behavior for Sparrow
    }
}

Enter fullscreen mode Exit fullscreen mode
public class Ostrich extends Bird {
     // Ostrich cannot fly, so we don't override fly()

}
Enter fullscreen mode Exit fullscreen mode

In this case, the Ostrich class violates the rule. We can refactor it. Here's a corrected example that better illustrates the principle:

public interface Flyable {
    void fly();
}
Enter fullscreen mode Exit fullscreen mode
public class Bird {
    void move() {
        // Default move behavior
    }
}
Enter fullscreen mode Exit fullscreen mode
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
    }
}
Enter fullscreen mode Exit fullscreen mode
public class Ostrich extends Bird {
    @Override
    void move() {
        // Specific running behavior for Ostrich
        System.out.println("Ostrich is running.");
    }
}
Enter fullscreen mode Exit fullscreen mode
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();
    }
}

Enter fullscreen mode Exit fullscreen mode

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:

public interface Worker {
    void work();
    void eat();
}
Enter fullscreen mode Exit fullscreen mode

Now, let's say we have two classes, Engineer and Waiter, that implement this interface:

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

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

Enter fullscreen mode Exit fullscreen mode
public class Waiter implements Worker {
    @Override
    public void work() {
        // Waiter-specific work
    }

    @Override
    public void eat() {
        // Waiter-specific eating
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

public interface Workable {
    void work();
}
Enter fullscreen mode Exit fullscreen mode
public interface Eatable {
    void eat();
}
Enter fullscreen mode Exit fullscreen mode

Now, our Engineer and Waiter classes can implement only the interfaces they need:

public class Engineer implements Workable {
    @Override
    public void work() {
        // Engineer-specific work
    }
}
Enter fullscreen mode Exit fullscreen mode
public class Waiter implements Workable, Eatable {
    @Override
    public void work() {
        // Waiter-specific work
    }

    @Override
    public void eat() {
        // Waiter-specific eating
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

public class LightBulb {
    void turnOn() {
        // Turn on the light bulb
    }
}
Enter fullscreen mode Exit fullscreen mode
public class Switch {
    private LightBulb bulb;

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

    void operate() {
        bulb.turnOn();
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, Switch directly depends on LightBulb, violating DIP. We can introduce an abstraction to decouple them:

Solution:

public interface Switchable {
    void turnOn();
}
Enter fullscreen mode Exit fullscreen mode
public class LightBulb implements Switchable {
    @Override
    public void turnOn() {
        // Turn on the light bulb
    }
}
Enter fullscreen mode Exit fullscreen mode
public class Switch {
    private Switchable device;

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

    void operate() {
        device.turnOn();
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
cicirello profile image
Vincent A. Cicirello

Hi. Consider using syntax highlighting with your code examples. It will improve readability. Here is how to do it.

```Java
public class Foo {

}
```
Enter fullscreen mode Exit fullscreen mode

The above produces:

public class Foo {

}
Enter fullscreen mode Exit fullscreen mode

It works for most languages. Just change Java to relevant language name.

Collapse
 
pathus90 profile image
Diallo Mamadou Pathé

Thank you.