设计模式:002_设计模式七大原则

63 阅读10分钟

设计模式七大原则

  • 开闭原则(Open Close Principle)
  • 单一职责原则(Single Responsibility Principle)
  • 接口隔离原则(Interface Segregation Principle)
  • 依赖倒置原则(Dependence Inversion Principle)
  • 里氏替换原则(Liskov Substitution Principle)
  • 迪米特法则(最少知道原则)(Law Of Demeter)
  • 合成复用原则(Composite Reuse Principle)

开闭原则

定义:一个软件实体如类、模块和函数应该对扩展开放(提供方可扩展),对修改关闭(使用方不用修改)。强调的是用抽象构建框架,用实现扩展细节。

  • 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
  • 编程中遵循其他原则,以及使用设计模式的目的就是遵循开闭原则。
  • 实现开闭原则的核心思想是面向抽象编程。

违反开闭原则:

// 违反开闭原则,新增图形使用方 DrawEditor 需要修改
public class Shape {
    int type;
}

public class Rect extends Shape {
    public Rect() {
        super.type = 1;
    }
}

public class Circle extends Shape {
    public Circle() {
        super.type = 2;
    }
}

// 作为使用方,新增图形需要修改
public class DrawEditor {

    public void drawShape(Shape shape) {
        if (shape.type == 1) {
            drawRect();
        } else if (shape.type == 2) {
            drawCircle();
        }
    }

    private void drawRect() {
        System.out.println("绘制矩形");
    }

    private void drawCircle() {
        System.out.println("绘制圆形");
    }
}

遵循开闭原则:

// 遵循开闭原则,新增图形使用方 DrawEditor 不需要修改
abstract class Shape {
    int type;

    public abstract void draw();
}

public class Rect extends Shape {
    public Rect() {
        super.type = 1;
    }

    @Override
    public void draw() {
        System.out.println("绘制矩形");
    }
}

public class Circle extends Shape {
    public Circle() {
        super.type = 2;
    }

    @Override
    public void draw() {
        System.out.println("绘制圆形");
    }
}

// 作为使用方,新增图形不需要修改
public class DrawEditor {
    public void drawShape(Shape shape) {
        shape.draw();
    }
}

单一职责原则

定义:不要存在多于一个导致类变更的原因;保持一个类/接口/方法只负责一项职责。

  • 降低类的复杂度,一个类只负责一项职责
  • 提高类的可读性,可维护性
  • 降低变更引起的风险
  • 大部分情况应当遵守单一职责原则,当类逻辑相当简单,可以在类层面违反单一职责原则;当类中方法数量足够少,可以在方法级别违反单一职责原则
// 类级别遵守单一职责原则
public class AnimalCat {
    public void eat(String name) {
        System.out.println(name + "吃猫粮");
    }
}

public class AnimalDog {
    public void eat(String name) {
        System.out.println(name + "吃狗粮");
    }
}

public class AnimalRabbit {
    public void eat(String name) {
        System.out.println(name + "吃兔粮");
    }
}
// 方法级别遵守单一职责原则
public class Animal {
    public void catEat(String name) {
        System.out.println(name + "吃猫粮");
    }

    public void dogEat(String name) {
        System.out.println(name + "吃狗粮");
    }

    public void rabbitEat(String name) {
        System.out.println(name + "吃兔粮");
    }
}

接口隔离原则

定义:用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。

  • 一个类对应一个类的依赖应该建立在最小的接口上
  • 建立单一接口,不要建立庞大臃肿的接口
  • 尽量细化接口,接口中的方法尽量少
  • 适度原则,一定要适度

违反接口隔离原则:

// 违反接口隔离原则示例;接口臃肿,方法过多
// 使用者 StudentA 只需要 wakeUp()/sleep()/schoolStart()/schoolEnd();但其实现类 Student 要实现接口的全部方法
// 使用者 WorkerA 只需要 wakeUp()/sleep()/workStart()/workEnd();但其实现类 Worker 要实现接口的全部方法
public interface Person {
    void wakeUp();

    void sleep();

    void schoolStart();

    void schoolEnd();

    void workStart();

    void workEnd();
}

public class Student implements Person {
    @Override
    public void wakeUp() { }

    @Override
    public void sleep() { }

    @Override
    public void schoolStart() { }

    @Override
    public void schoolEnd() { }

    @Override
    public void workStart() { }

    @Override
    public void workEnd() { }
}

public class Worker implements Person {
    @Override
    public void wakeUp() { }

    @Override
    public void sleep() { }

    @Override
    public void schoolStart() { }

    @Override
    public void schoolEnd() { }

    @Override
    public void workStart() { }

    @Override
    public void workEnd() { }
}

public class StudentA {
    public void wakeUp(Student student) {
        student.wakeUp();
    }

    public void sleep(Student student) {
        student.sleep();
    }

    public void schoolStart(Student student) {
        student.schoolStart();
    }

    public void schoolEnd(Student student) {
        student.schoolEnd();
    }
}

public class WorkerA {
    public void wakeUp(Worker worker) {
        worker.wakeUp();
    }

    public void sleep(Worker worker) {
        worker.sleep();
    }

    public void workStart(Worker worker) {
        worker.workStart();
    }

    public void workEnd(Worker worker) {
        worker.workEnd();
    }
}

遵守接口隔离原则:

// 遵守接口隔离原则示例;拆分接口,接口中的方法尽量少,使用者不用依赖它不需要的接口
public interface Life {
    void wakeUp();

    void sleep();
}

public interface School {
    void schoolStart();

    void schoolEnd();
}

public interface Work {
    void workStart();

    void workEnd();
}

public class Student implements Life, School {
    @Override
    public void wakeUp() { }

    @Override
    public void sleep() { }

    @Override
    public void schoolStart() { }

    @Override
    public void schoolEnd() { }
}

public class Worker implements Life, Work {
    @Override
    public void wakeUp() { }

    @Override
    public void sleep() { }

    @Override
    public void workStart() { }

    @Override
    public void workEnd() { }
}

public class StudentA {
    public void wakeUp(Student student) {
        student.wakeUp();
    }

    public void sleep(Student student) {
        student.sleep();
    }

    public void schoolStart(Student student) {
        student.schoolStart();
    }

    public void schoolEnd(Student student) {
        student.schoolEnd();
    }
}

public class WorkerA {
    public void wakeUp(Worker worker) {
        worker.wakeUp();
    }

    public void sleep(Worker worker) {
        worker.sleep();
    }

    public void workStart(Worker worker) {
        worker.workStart();
    }

    public void workEnd(Worker worker) {
        worker.workEnd();
    }
}

依赖倒置原则

定义:依赖抽象,而不是依赖细节。

  • 高层模块不应该依赖低层模块,两者都应该依赖于抽象
  • 抽象不应该依赖细节,细节应该依赖抽象
  • 依赖倒置的中心思想是面向接口编程
  • 相较于细节的多变性,抽象要稳定的多;以抽象为基础搭建的架构,比以细节为基础搭建的架构要稳定的多。抽象指的是接口或抽象类,细节指的是具体的实现类。
  • 使用接口或抽象类的目的是制定规范,而不涉及任何具体的操作,展现细节的任务交给实现类完成

依赖倒置原则注意事项:

  • 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性会更好
  • 变量的声明类型尽量是抽象类或接口,利于程序扩展和优化
  • 继承时遵守里氏替换原则

违反依赖倒置原则:

// 违反依赖倒置原则示例;没有依赖抽象,而是依赖细节
public class Car {
    public void run() { }
}

public class Subway {
    public void run() { }
}

public class Driver {
    public void operationCar(Car car) {
        car.run();
    }

    public void operationSubway(Subway subway) {
        subway.run();
    }
}

遵守依赖倒置原则:

// 遵守依赖倒置原则示例;依赖抽象,没有依赖细节
public interface Vehicle {
    void run();
}

public class Car implements Vehicle {
    @Override
    public void run() { }
}

public class Subway implements Vehicle {
    @Override
    public void run() { }
}

public class Driver {
    public void operation(Vehicle vehicle) {
        vehicle.run();
    }
}

依赖关系传递的三种方式

  • 通过接口传递
public interface IDriver {
    // 在接口方法中传递
    void operation(IVehicle vehicle);
}

public interface IVehicle {
    void run();
}

public class Driver implements IDriver {
    @Override
    public void operation(IVehicle vehicle) {
        vehicle.run();
    }
}
  • 通过构造方法传递
public interface IDriver {
    void operation();
}

public interface IVehicle {
    void run();
}

public class Driver implements IDriver {
    private IVehicle vehicle;

    // 在构造方法中传递
    public Driver(IVehicle vehicle) {
        this.vehicle = vehicle;
    }

    @Override
    public void operation() {
        this.vehicle.run();
    }
}
  • 通过 setter 方法传递
public interface IDriver {
    void operation();
}

public interface IVehicle {
    void run();
}

public class Driver implements IDriver {
    private IVehicle vehicle;

    // 在 setter 方法中传递
    public void setVehicle(IVehicle vehicle) {
        this.vehicle = vehicle;
    }

    @Override
    public void operation() {
        this.vehicle.run();
    }
}

里氏替换原则

定义:如果调用一个父类的方法可以成功运行,那么替换成子类调用也应该完全可以运行。

  • 子类可以扩展父类的功能,但不能改变父类原有的功能。
  • 子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

违反里式替换原则:

public class Calculator {
    public int addition(int a, int b) {
        return a + b;
    }
}

public class Count extends Calculator {
    // 违反里式替换原则,重写了父类的方法,改变了父类原有的功能,导致出错
    @Override
    public int addition(int a, int b) {
        return a * b;
    }

    public int subtraction(int a, int b) {
        return a - b;
    }
}

里式替换原则告诉我们,继承实际上让两个类耦合性增强,在适当情况下,可以通过聚合、组合、依赖来解决问题。

遵守里式替换原则:

// 遵守里式替换原则
// 创建更加基础的基类,都继承自基类
public class Base {
}

public class Calculator extends Base {
    public int addition(int a, int b) {
        return a + b;
    }
}

public class Count extends Base {
    // 与 Calculator 不再是继承关系,而是组合关系;降低耦合性
    private final Calculator calculator = new Calculator();

    public int addition(int a, int b) {
        return calculator.addition(a, b);
    }

    public int subtraction(int a, int b) {
        return a - b;
    }
}

迪米特法则(最少知道原则)

定义:一个对象对其他对象保持最少的了解,尽量降低类与类之间的耦合,强调只与直接的朋友交流。

  • 迪米特法则又称:只与直接的朋友交流。
  • 直接的朋友:成员变量、方法的参数、方法的返回值。
  • 方法中的局部变量不是直接的朋友。因此,陌生的类最好不要以局部变量的形式出现在类内部。
  • 类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对令一个类的影响也越大。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。
  • 也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量的将逻辑封装在类的内部,对外除了提供的 public 方法,不对外泄露任何信息。

违反迪米特法则:

public class HighSchoolStudent {
    private String id;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
}

public class SchoolStudent {
    private String id;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
}

public class HighSchoolManager {
    public List<HighSchoolStudent> getAllStudent() {
        List<HighSchoolStudent> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            HighSchoolStudent student = new HighSchoolStudent();
            student.setId("HighSchoolStudent:" + i);
            list.add(student);
        }
        return list;
    }
}

public class SchoolManager {
    public List<SchoolStudent> getAllStudent() {
        List<SchoolStudent> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            SchoolStudent student = new SchoolStudent();
            student.setId("SchoolStudent:" + i);
            list.add(student);
        }
        return list;
    }

    public void printAllStudent(HighSchoolManager manager) {
        // HighSchoolStudent 不是 SchoolManager 的直接朋友
        // HighSchoolStudent 以局部变量的方式出现在 SchoolManager 类内部
        // 违反迪米特法则,需要优化
        List<HighSchoolStudent> highStudents = manager.getAllStudent();
        for (HighSchoolStudent student : highStudents) {
            System.out.println(student.getId());
        }
        List<SchoolStudent> middleStudents = this.getAllStudent();
        for (SchoolStudent student : middleStudents) {
            System.out.println(student.getId());
        }
    }
}

遵守迪米特法则:

public class HighSchoolStudent {
    private String id;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
}

public class SchoolStudent {
    private String id;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
}

public class HighSchoolManager {
    public List<HighSchoolStudent> getAllStudent() {
        List<HighSchoolStudent> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            HighSchoolStudent student = new HighSchoolStudent();
            student.setId("HighSchoolStudent:" + i);
            list.add(student);
        }
        return list;
    }

    public void printAllStudent() {
        List<HighSchoolStudent> highStudents = this.getAllStudent();
        for (HighSchoolStudent student : highStudents) {
            System.out.println(student.getId());
        }
    }

}

public class SchoolManager {
    public List<SchoolStudent> getAllStudent() {
        List<SchoolStudent> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            SchoolStudent student = new SchoolStudent();
            student.setId("SchoolStudent:" + i);
            list.add(student);
        }
        return list;
    }

    public void printAllStudent(HighSchoolManager manager) {
        // 遵守迪米特法则,只与直接朋友交流
        manager.printAllStudent();
        List<SchoolStudent> middleStudents = this.getAllStudent();
        for (SchoolStudent student : middleStudents) {
            System.out.println(student.getId());
        }
    }
}

合成复用原则

定义:尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的。

合成复用原则又叫组合/聚合复用原则,它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。

  • 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  • 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  • 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。

  • 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  • 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  • 合成复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

违反合成复用原则;使用继承关系,达到软件复用的目的。

public class Car {
    public void run() {
        System.out.println("Car run");
    }
}

public class Driver extends Car {
    @Override
    public void run() {
        super.run();
    }
}

遵守合成复用原则;使用依赖关系,达到软件复用的目的。

public class Car {
    public void run() {
        System.out.println("Car run");
    }
}

public class Driver {
    public void run(Car car) {
        car.run();
    }
}

遵守合成复用原则;使用聚合关系,达到软件复用的目的。

public class Car {
    public void run() {
        System.out.println("Car run");
    }
}

public class Driver {
    private Car car;
    public void setCar(Car car) {
        this.car = car;
    }
    public void run() {
        this.car.run();
    }
}

遵守合成复用原则;使用组合关系,达到软件复用的目的。

public class Car {
    public void run() {
        System.out.println("Car run");
    }
}

public class Driver {
    private Car car = new Car();
    public void run() {
        this.car.run();
    }
}