/
Tech-study-notes

SOLID Principle

In software programming, SOLID is an acronym for 5 design principles intended to make object-oriented designs more understandable, flexible, and maintainable.


S = Single Responsibility (SRP)

Classes and objects should have one, and only one, responsibility (reason to change).

This separation of responsibilities makes code more modular, more maintainable, less complex, and promotes the reduction of bugs due to changes in one responsibility that may affect another.

Before:

public class Employee {
    private String name;
    private double salary;
    // getter and setter methods

    public void saveToDatabase() {
        // saves the employee's data to the database
    }

    public void generatePaySlip() {
        // generates the employee's payslip
    }

    public void calculateSalary() {
        // calculates the employee's salary
    }
}

After:

public class Employee {
    private String name;
    private double salary;
    // getter and setter methods
}

public class DatabaseManager {
    public void saveEmployee(Employee employee) {
        // save employee to db
    }
}

public class PaySlipGenerator {
    public void generatePaySlip(Employee employee) {
        // generate pay slip of employee
    }
}

public class SalaryCalculator {
    public double calculateSalary(Employee employee) {
        // calculate salary of employee
        // ...
        return salary;
    }
}

O = Open Closed (OCP)

Entities should be open for extension (can be extended by adding what is needed), but closed for modification (should never be modified).

On a practical standpoint, classes are “open” to extensions via interface implementation or inheritance, but “closed” to modifications, since the existing behavior remains unchanged. This significantly reduces the risk of breaking existing functionality and provides a looser coupling.

interface Vehicle {
    double calculateTaxCost();
}

class Car implements Vehicle {
    private double engineCapacity;

    public Car(double engineCapacity) {
        this.engineCapacity = engineCapacity;
    }

    public double getEngineCapacity() {
        return engineCapacity;
    }

    public double calculateTaxCost() {
        return engineCapacity * 2.5;
    }
}

class Motorcycle implements Vehicle {
    private double engineCapacity;

    public Motorcycle(double engineCapacity) {
        this.engineCapacity = engineCapacity;
    }

    public double getEngineCapacity() {
        return engineCapacity;
    }

    public double calculateTaxCost() {
        return engineCapacity * 1.5;
    }
}

class TaxCalculator {
    public double calculateTotalTax(List<Vehicle> vehicles) {
        double totalTax = 0;
        for (Vehicle vehicle : vehicles) {
            totalTax += vehicle.calculateTaxCost();
        }
        return totalTax;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Vehicle> vehicles = new ArrayList<>();
        vehicles.add(new Car(2000));
        vehicles.add(new Motorcycle(500));

        TaxCalculator taxCalculator = new TaxCalculator();
        double totalTax = taxCalculator.calculateTotalTax(vehicles);

        System.out.println("Total tax: " + totalTax);
    }
}

In this example, if we wanted to add new vehicle types (e.g. truck), we would not have to modify the TaxCalculator class. We would simply have to create a new class that implements the Vehicle interface and provide an implementation of the calculateTaxCost() method. The TaxCalculator class would remain unchanged and would be able to calculate taxes for new vehicle types without any modifications.


L = Liskov Substitution (LSP)

If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.

When a child Class cannot perform the same actions as its parent Class, this can cause bugs. If you have a Class and create another Class from it, it becomes a parent and the new Class becomes a child. The child Class should be able to do everything the parent Class can do. This process is called Inheritance.

Before:

@Getter
@Setter
@ToString
public class Bird {
    private String name;

    public Bird(String name) {
        this.name = name;
    }

    public void fly() {
        System.out.println(getName() + " is flying.");
    }
}

@Getter
@Setter
@ToString(callSuper = true)
public class Penguin extends Bird {
    public Penguin(String name) {
        super(name);
    }

    // Penguin is a Bird that cannot fly, so using the fly method violates the Liskov Substitution Principle. This method should be removed.
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins cannot fly!");
    }
}

After:

public interface FlyingBird {
    void fly();
}

@Getter
@Setter
@ToString
public class Bird {
    private String name;

    public Bird(String name) {
        this.name = name;
    }
}

@Getter
@Setter
@ToString(callSuper = true)
public class Eagle extends Bird implements FlyingBird {
    public Eagle(String name) {
        super(name);
    }

    @Override
    public void fly() {
        System.out.println(getName() + " is flying.");
    }
}

@Getter
@Setter
@ToString(callSuper = true)
public class Penguin extends Bird {
    public Penguin(String name) {
        super(name);
    }

    // Penguin does not implement the FlyingBird interface because it cannot fly.
}

I = Interface Segregation (ISP)

Clients should not be forced to depend on methods that they do not use.

Classes that implement interfaces, should not be forced to implement methods they do not use. Big interfaces should be split into smaller ones so there are no methods that are not used. Classes know only about methods related to them providing decoupling and easier modifications.

Before:

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

public class Robot implements Worker {

    @Override
    public void work() {
        // implementation
    }

    @Override
    public void rest() {
        throw new UnsopportedOperationException();
    }
}

public class Man implements Worker {

    @Override
    public void work() {
        // implementation
    }

    @Override
    public void rest() {
        // implementation
    }
}

After:

public interface Work {
    void work();
}

public interface Rest {
    void rest();
}

public class Robot implements Work {

    @Override
    public void work() {
        // implementation
    }
}

public class Man implements Work, Rest {

    @Override
    public void work() {
        // implementation
    }

    @Override
    public void rest() {
        // implementation
    }
}

D = Dependency Inversion (DIP)

High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

Code is much more reusable and easier to maintain if abstractions and details are isolated from each other. This goes both ways:

Before:

public class Radio {
    public string playSong() {
        return "random song from the radio"
    }
}

public class MusicListener {
    private Radio radio;

    public MusicListener() {
        this.radio = new Radio();
    }

    public void listen() {
        String song = this.radio.playSong();
        System.out.println(song);
    }
}

After:

public class MusicSource {
    string playSong();
}

public class Radio implements MusicSource {
    @Override
    public string playSong() {
        return "random song from the radio"
    }
}

public class MP3Device implements MusicSource {
    @Override
    public string playSong() {
        return "random song from the MP3 device"
    }
}

public class MusicListener {
    private MusicSource source;

    public MusicListener(MusicSource source) {
        this.source = source;
    }

    public void listen() {
        String song = this.source.playSong();
        System.out.println(song);
    }
}

In this example, if you want to change the source of music, you just need to pass a different instance of MusicSource to MusicListener, without editing the MusicListener class.