设计模式:简介、OOP七大设计原则


23种设计模式

简介

设计模式(Design pattern)是对软件设计中普遍存在问题的解决方案。

这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

目的

  1. 提高思维、编程和设计能力;
  2. 使程序设计更标准化、代码编制更工程化,提高开发效率,缩短开发周期;
  3. 使代码具有更好的:重用性、可读性、可扩展性(可维护性)、可靠性、高内聚低耦合

设计模式(GoF23)

  • 创建型
    • 单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式;
  • 结构型
    • 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式
  • 行为型
    • 模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。

OOP七大设计原则

在学习设计原则和设计模式之前,需要先了解 。

设计原则

  • 设计模式的基础,即设计的依据;
  • 封装继承多态,以及类的关联关系和组合关系的充分理解。
  1. 单一职责原则 (Singel Responsibility)
  2. 接口隔离原则 (Interface Segregation)
  3. 合成复用原则 (Composite Reuse)
  4. 里式替换原则 (Liskov Substitution)
  5. 依赖倒置原则 (Dependence Inversion)
  6. 迪米特法则 (Demeter)
  7. 开闭原则:对扩展开放,对修改关闭;

1、单一职责(Single Responsibility)

简介

  • 降低类的复杂度,一个类只负责一项职责
  • 提高类的可读性、可维护性,降低变更引起的风险;
  • 分解类的粒度,高内聚低耦合;

实例

  • 类A:负责 职责1职责2
  • 问题:当修改 职责1 的代码时,可能对 职责2 的代码造成影响;
  • 解决:将 类A 的粒度分解为A1,A2(即分为两个类)。

2、接口隔离(Interface Segregation)

简介

  • 客户端不应该依赖它不需要的接口,即类对另一个类的依赖应建立在最小的接口上
  • 为各个类建立它们需要的专门接口;

实例

  • 接口F:有 5 个方法(Method1,2,3,4,5),2 个实现类(FImpl1FImpl2
  • 类A:通过接口F 依赖(使用)类FImpl1 中的 3 个方法(Method1,2,3
  • 类B:通过接口F 依赖(使用)类FImpl2 中的 3 个方法(Method1,4,5
  • 问题:接口F 对于类A 和 类B 来说不是最小接口,则实现类FImpl1 和 类FImpl2 需要实现它们不需要的方法;

  • 解决:将接口F 拆分为独立的 3 个接口,类A 和 类B 分别与需要的接口建立依赖关系。

    image-20211201015043745

3、合成复用(Composite Reuse)

优先使用聚合 / 组合关系,其次考虑继承关系。

4、里式替换(Liskov Substitution)

简介

  1. 引用父类的地方,必须能透明地使用其子类的对象。即继承必须确保父类的性质在子类中成立
  2. 子类尽量不要重写父类方法,否则会降低继承体系的复用性,特别是对于多态的使用;
  3. 继承会提高耦合性,通用做法:改用依赖/聚合/组合等关系来替换继承关系;

实例

  • 父类Calc1:有方法 operation1(int x, int y),返回两数之和
  • 子类Calc2:继承父类Calc1
    • (可能是无意间)重写方法operation1(),返回两数之差;
    • 添加新方法operation2(int x, int y):返回两数之积
  • 问题:在引用父类时使用多态Calc1 calc = new Calc2()]】,调用方法operation1() 欲计算两数之和,而实际运行时返回两数之差;

  • 解决:将原来的父类和子类都继承一个更基础的基类BaseCalc,改用组合关系来代替继承关系。

    image-20211201143507723

5、依赖倒置(Dependence Inversion)

简介

  1. 中心思想面向接口编程,而不是面向实现编程;
  2. 变量的声明类型是抽象类或接口(在变量引用和实际对象之间“加一层”)
  3. 注意
    • 低层模块最好有抽象类或接口;
    • 高层模块不依赖低层模块,二者都应依赖其抽象;
    • 抽象不依赖于细节,细节应依赖于抽象;
    • 继承:遵循里式替换原则

实例

  • Person类:实现接收消息的功能;
  • Email类:接收电子邮件消息;
  • 问题:若增加需求(接收微信、QQ消息),需要新增对应的类和Person中对应接收方法;

  • 解决:声明接口 MessageReceiver,消息接收类(Email、微信、QQ)实现该接口,则Person类只需与接口建立依赖关系即可

    image-20211201143741834

依赖注入方式(!)

3 种依赖注入方式

  • Spring IOC DI:构造器注入、setter注入
  1. 构造器注入:声明变量时,向构造方法传递参数;
  2. setter注入:声明变量后,通过 setter设置参数;
  3. 调用时注入:调用方法时,向方法传递参数;

实例

// 待注入接口
interface TV{...}

// 1、构造器注入
interface mySwitch{
    private TV tv;
    public mySwitch(TV tv){
        this.tv = tv;
    }
}
// 2、setter注入
interface mySwitch{
    private TV tv;
    public void setTv(Tv tv){
        this.tv = tv;
    }
    
// 3、调用时注入
interface mySwitch{
    public void turnOn(TV tv);
	}
}

6、迪米特(Demeter)

简介

  1. 迪米特法则(又称“最少知道原则”),即一个类对自己依赖的类知道的越少越好;
  2. 被依赖的类:代码逻辑都封装在类内部,对外只提供 public方法。
  3. 更简单的定义只与直接朋友交流
    • 类的依赖关系:成员变量(聚合 / 组合)、方法参数、方法返回值、局部变量
    • 直接朋友:成员变量(聚合 / 组合)、方法参数、方法返回值;
    • 陌生朋友局部变量

实例

  • Student类:属性 (id);
  • Teacher类:属性 (id);
  • StudentManage类:方法 (获取所有学生信息);
  • TeacherManage类:方法(获取所有老师、输出所有老师和学生的信息)
  • 问题:TeacherManage类中的 “输出所有学生”方法,使用到了成员变量Student(陌生朋友),违背迪米特法则;

  • 解决:在 StudentManage类中声明一个方法(原本在 TeacherManage类中的 “输出所有学生”方法)。而TeacherManage类中通过方法参数调用StudentManage类中的 “输出所有学生”方法。

    image-20211201213622912

7、开闭(Open Close)

简介

开闭原则 (OCP):编程中最基础、最重要的设计模式

  1. 对扩展开放(对提供方),对修改关闭(对使用方)
  2. 用抽象构建框架,用实现扩展实现;
  3. 当需要变化时,通过扩展软件实体的行为,而不是通过修改已有代码;
  4. 所有设计原则和设计模式,目的就是遵循开闭原则;

实例

  • GraphEditor类:绘图方法 (用于判断绘制哪个图形)、绘图方法 (各个图形的绘制方法)
  • 父类Shape:属性 (id)
  • 子类Circle:属性 (id)
  • 子类Rectangle:属性 (id)
  • 本例中,GraphEditor类是使用方,Shape类是提供方

  • 问题:当新增一个需求时(如增加图形 Triangle),需要增加 Shape子类、在GraphEditor类中增加分支、增加绘图方法;

  • 解决:将Shape类声明为抽象类,声明一个抽象方法用于绘图(原本在GraphEditor类中的 “绘图方法”),由子类实现该方法。而 GraphEditor类只需通过多态调用方法即可,无需通过分支判断 id。

    image-20211201225444363