SOLID Principle - MacKittipat/note-design-pattern GitHub Wiki

Single-responsibility principle

  • A class should have one and only one reason to change
  • Meaning that a class should only have one job.
  • Solution : Aim for high cohesion and low coupling
    • High cohesion : The degree to which the elements inside a module belong together
    • Low coupling : The degree of interdependence between software modules

Benefit :

  • Class can be reusable.
  • Easy to find where the problem come from

Open-Closed Principle

  • Classes should be open for extension but closed for modification.
  • Closed for modification means that once you have developed a class you should never modify it, except to correct bugs.
public class Warrior {
    public void attack(String weapon) {
        if("sword".equals(weapon)) {
            System.out.println("Slash !");
        } else if ("bow".equals(weapon)) {
            System.out.println("Shoot !");
        } else if ("knife".equals(weapon)) {
            System.out.println("Stab !");
        }
        // More weapon will be add here. This is not good.
    }
}

// TO BE 

public class Warrior {
    public void attack(Weapon weapon) {
        weapon.use();
    }
}

interface Weapon {
    void use();
}

class Sword implements Weapon {
    public void use() {
        System.out.println("Slash !");
    }
}

class Bow implements Weapon {
    public void use() {
        System.out.println("Shoot !");
    }
}

class Knife implements Weapon {
    public void use() {
        System.out.println("Stab !");
    }
}

Liskov substitution principle

  • Ability to replace any instance of parent class with an instance of one of its child classes without negative side effects.
  • Any child type of a parent type should be able to stand in for that parent without things blowing up.
  • Derived class must be usable through the base class interface, without the need for the user to know the different.
  • Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program
    • An example of this is with a Bird base class. You might assume that it should have a fly method. But what about the birds that can’t fly? Like a Penguin. In this example, fly should not be in the base class as it does not apply to all subclasses.
public class App {
    public static void main(String[] args) {
        Rectangle r = new Rectangle();
        r.setHeight(10);
        r.setWidth(5);
        System.out.println(r.area()); // 50

        r = new Square();
        r.setHeight(10);
        r.setWidth(5);
        System.out.println(r.area()); // 50, Wrong because Height and width of Square must equal.
    }
}

class Rectangle {
    private int width;
    private int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int area() {
        return width * height;
    }
}

class Square extends Rectangle {

}

// TO BE

public class App {
    public static void main(String[] args) {
        Rectangle r = new Rectangle();
        r.setHeight(10);
        r.setWidth(5);
        System.out.println(r.area()); // 50

        r = new Square();
        r.setHeight(10);
        r.setWidth(5);
        System.out.println(r.area()); // 25
    }
}

class Rectangle {
    private int width;
    private int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int area() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

Interface segregation principle

  • Clients should not be forced to implement interface that they does not use.
  • Create multiple, smaller, cohesive interfaces.
  • Cohesive, meaning they have groups of operations that logically belong together.

Benefit :

  • Client class don't have to override unnecessary method from Interface.

interface Cat {
    void walk();
    void eat();
}

class PersianCat implements Cat {
    public void walk() {
        System.out.println("PersianCat is walking");
    }

    public void eat() {
        System.out.println("PersianCat is eating");
    }
}

class RobotCat implements Cat {
    public void walk() {
        System.out.println("RobotCat is walking");
    }

    public void eat() {
        // Robot cat don't really need to eat.
    }
}

// TO BE 

interface Walkable {
    void walk();
}

interface Eatable {
    void eat();
}

class PersianCat implements Walkable, Eatable {
    public void walk() {
        System.out.println("PersianCat is walking");
    }

    public void eat() {
        System.out.println("PersianCat is eating");
    }
}

class RobotCat implements Walkable {
    public void walk() {
        System.out.println("RobotCat is walking");
    }
}

Dependency inversion principle

  • High level modules should not depend on low level modules, both should depend on abstractions.
    • High level module is a module which depends on other modules. For example, UserRestController that depends on UserService.
  • Abstractions should not depend on details. Details should depend upon abstractions.
  • Use dependency injection to reduce coupling between class.
  • While dependency injection is a design pattern that allows us to separate creation from use.

Benefit :

  • Easy to write unit test. We can mock low level modules
  • Easy to change when high level module want to change low level module.

class StorageService {

    public void store(Object o) {
        // StorageService depend on FileStorage. 
        FileStorage fs = new FileStorage(); 
        fs.save(o);
    }
}

class FileStorage {
    public void save(Object o) {
    }
}

// TO BE

class StorageService {
    private Storage storage;

    public void setStorage(Storage storage) {
        this.storage = storage;
    }

    public void store(Object o) {
        storage.save(o);
    }
}

interface Storage {
    void save(Object o);
}

class FileStorage implements Storage {
    public void save(Object o) {
    }
}


Summary

  • Single-responsibility Principle : A class should have only one job
  • Open-Closed Principle : A class should open for extension and close for mofication
  • Liskov Substitution Principle : Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program
  • Interface segregation principle : Create small interface with minimal number of method.
  • Dependency inversion : High level modules should not depend on low level modules, both should depend on abstractions to reduce coupling between module

Reference