By kswaughs | Thursday, August 22, 2024

SOLID Principles Java Example

1. Single Responsibility Principle (SRP)

2. Open/Closed Principle (OCP)

3. Liskov Substitution Principle (LSP)

4. Interface Segregation Principle (ISP)

5. Dependency Inversion Principle (DIP)

1. Single Responsibility Principle (SRP)

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

Example

// Class responsible for handling user data
public class User {
    private String name;
    private String email;

    // Getters and setters
}

// Class responsible for user persistence
public class UserRepository {
    public void save(User user) {
        // Code to save user to database
    }
}

2. Open/Closed Principle (OCP)

It states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that the behavior of a module can be extended without modifying its source code.

Key Points
1. Open for Extension: You should be able to add new functionality to the module.
2. Closed for Modification: You should not change the existing code of the module.

Consider a scenario where you have a `Shape` class and you want to calculate the area of different shapes like `Circle` and `Rectangle`. Initially, you might have a single class with a method that handles all shapes, which violates the Open/Closed Principle.

Bad Example (Violating OCP)

public class Shape {
    public enum Type { CIRCLE, RECTANGLE }

    private Type type;
    private double radius;
    private double width;
    private double height;

    public Shape(Type type, double radius, double width, double height) {
        this.type = type;
        this.radius = radius;
        this.width = width;
        this.height = height;
    }

    public double calculateArea() {
        switch (type) {
            case CIRCLE:
                return Math.PI * radius * radius;
            case RECTANGLE:
                return width * height;
            default:
                throw new UnsupportedOperationException("Shape type not supported");
        }
    }
}

In this example, if you want to add a new shape, you need to modify the `Shape` class, which violates the Open/Closed Principle.

Good Example (Adhering to OCP):

  • Define an abstract `Shape` class with an abstract `calculateArea` method.
  • Create concrete classes for each shape that extend the `Shape` class and implement the `calculateArea` method.
Good Example (Adhering to OCP)

// Abstract Class:

public abstract class Shape {
    public abstract double calculateArea();
}

// Concrete Classes:

public class Circle extends Shape {
    private double radius;

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

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

public class Rectangle extends Shape {
    private double width;
    private double height;

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

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

// Usage:

public class Main {
    public static void main(String[] args) {
        Shape circle = new Circle(5);
        Shape rectangle = new Rectangle(4, 6);

        System.out.println("Circle Area: " + circle.calculateArea());
        System.out.println("Rectangle Area: " + rectangle.calculateArea());
    }
}

In this refactored example, the `Shape` class is open for extension (you can add new shapes by creating new subclasses) but closed for modification (you don't need to change the existing `Shape` class to add new shapes).

3. Liskov Substitution Principle (LSP)

It states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In other words, if class `S` is a subclass of class `T`, then objects of type `T` should be replaceable with objects of type `S` without altering the desirable properties of the program (correctness, task performed, etc.).

Key Points
1. Subtypes must be substitutable for their base types: Derived classes must be substitutable for their base classes.
2. Behavioral compatibility: Subtypes must behave in a way that does not violate the expectations established by the base type.

Consider a scenario where you have a base class `Bird` and a subclass `Ostrich`. According to LSP, the `Ostrich` class should be able to replace the `Bird` class without causing issues.

Bad Example (Violating LSP)
// Base Class:

public class Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}


// Subclass Violating LSP:

public class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Ostrich can't fly");
    }
}

In this example, the `Ostrich` class violates the Liskov Substitution Principle because it changes the expected behavior of the `fly` method. To adhere to LSP, we should design our classes in a way that does not violate the expectations of the base class.

Good Example (Adhering to LSP)

//Base Class:

public abstract class Bird {
    public abstract void move();
}

// Subclass:

public class FlyingBird extends Bird {
    @Override
    public void move() {
        System.out.println("Bird is flying");
    }
}

public class Ostrich extends Bird {
    @Override
    public void move() {
        System.out.println("Ostrich is running");
    }
}

// Usage:

public class BirdWatcher {
    public void watchBird(Bird bird) {
        bird.move();
    }

    public static void main(String[] args) {
        BirdWatcher watcher = new BirdWatcher();
        Bird flyingBird = new FlyingBird();
        Bird ostrich = new Ostrich();

        watcher.watchBird(flyingBird); // Output: Bird is flying
        watcher.watchBird(ostrich);    // Output: Ostrich is running
    }
}

In this refactored example, both `FlyingBird` and `Ostrich` adhere to the Liskov Substitution Principle by providing their own implementation of the `move` method, which does not violate the expectations of the `Bird` class.

4. Interface Segregation Principle (ISP)

It states that no client should be forced to depend on methods it does not use. This means that larger interfaces should be split into smaller, more specific ones so that clients only need to know about the methods that are of interest to them.

Key Points
1. Clients should not be forced to implement interfaces they do not use: This avoids "fat" interfaces.
2. Interfaces should be client-specific: Each interface should be tailored to the specific needs of a client.

Consider a scenario where you have an interface `Worker` that has methods for different types of workers.

Bad Example (Violating ISP)

public interface Worker {
    void work();
    void eat();
}

public class HumanWorker implements Worker {

    @Override
    public void work() {
        System.out.println("Human is working");
    }

    @Override
    public void eat() {
        System.out.println("Human is eating");
    }
}

public class RobotWorker implements Worker {

    @Override
    public void work() {
        System.out.println("Robot is working");
    }

    @Override
    public void eat() {
        // Robot does not eat, but forced to implement this method
        throw new UnsupportedOperationException("Robot does not eat");
    }
}

In this example, the 'RobotWorker' class is forced to implement the `eat` method, which it does not need, violating the Interface Segregation Principle.

Good Example (Adhering to ISP)
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public class HumanWorker implements Workable, Eatable {
    @Override
    public void work() {
        System.out.println("Human is working");
    }

    @Override
    public void eat() {
        System.out.println("Human is eating");
    }
}

public class RobotWorker implements Workable {
    @Override
    public void work() {
        System.out.println("Robot is working");
    }
}

In this refactored example, the 'Worker interface is split into 'Workable' and 'Eatable' interfaces. Now, 'RobotWorker' only implements the 'Workable' interface, adhering to the Interface Segregation Principle.

5. Dependency Inversion Principle (DIP)

It states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details. Details should depend on abstractions.

Consider a scenario where you have a `Light` class and a `Switch` class. The `Switch` class directly depends on the `Light` class, which violates the Dependency Inversion Principle.

Bad Example (Violating DIP)

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

    public void turnOff() {
        System.out.println("Light is turned off");
    }
}

public class Switch {
    private Light light;

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

    public void operate(String command) {
        if (command.equalsIgnoreCase("ON")) {
            light.turnOn();
        } else if (command.equalsIgnoreCase("OFF")) {
            light.turnOff();
        }
    }
}

In this example, the `Switch` class directly depends on the `Light` class, which is a low-level module.

1. Define an abstraction for the `Switchable` interface. 2. Implement the `Switchable` interface in the `Light` class. 3. Modify the `Switch` class to depend on the `Switchable` interface.

Good Example (Adhering to DIP)

// Abstraction
public interface Switchable {
    void turnOn();
    void turnOff();
}

//Low-level Module
public class Light implements Switchable {

    @Override
    public void turnOn() {
        System.out.println("Light is turned on");
    }

    @Override
    public void turnOff() {
        System.out.println("Light is turned off");
    }
}

//High-level Module
public class Switch {
    private Switchable device;

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

    public void operate(String command) {
        if (command.equalsIgnoreCase("ON")) {
            device.turnOn();
        } else if (command.equalsIgnoreCase("OFF")) {
            device.turnOff();
        }
    }
}

In this refactored example, the `Switch` class depends on the `Switchable` interface, which is an abstraction. The `Light` class implements the `Switchable` interface. This way, the high-level module (`Switch`) does not depend on the low-level module (`Light`), adhering to the Dependency Inversion Principle.

Recommend this on