GoF23 & OOP 设计原则
GoF23
设计模式(Design pattern)
对软件设计中普遍存在问题的优秀解决方案。
好处:
- 提高思维、编程和设计能力。
- 使程序设计更标准化、代码编制更工程化,提高开发效率,缩短开发周期。
- 优化代码:重用性、可读性、可扩展性(可维护性)、可靠性、高内聚低耦合;
类型
- 创建型:工厂方法、抽象工厂、单例、建造者、原型。
- 结构型:装饰者、适配器、外观、组合、代理、桥接、享元。
- 行为型:观察者、命令、策略、模板方法、状态、迭代器、中介者、备忘录、访问者、职责链、解释器。
23 种设计模式
使用层次 | 变化 | 实现 | OOP原则 | |
---|---|---|---|---|
Factory Method | 代码级 | 对象实例化 | 对象的实例化推迟到子类 | 开闭、里氏替换、依赖倒置 |
Abstract Factory | 应用级 | 产品家族的扩展 | 封装产品族的创建 | 开闭、依赖倒置 |
Singleton | 代码级、应用级 | 唯一实例 | 封装对象产生的个数 | |
Observer | 应用级、构架级 | 通讯对象 | 封装对象通知 | 开闭 |
Decorator | 代码级 | 对象的组合职责 | 在稳定接口上扩展 | 开闭 |
Command | 应用级 | 请求的变化 | 封装命令对象 | 开闭 |
Adapter | 代码级 | 对象接口的变化 | 转换接口 | |
Facade | 应用级、构架级 | 子系统的高层接口 | 简化子系统 | 迪米特、开闭 |
Strategy | 应用级 | 算法的变化 | 封装算法 | 合成复用、里氏替换、开闭 |
Template Method | 代码级 | 算法子步骤的变化 | 封装算法结构(子步骤) | 依赖倒置 |
State | 应用级 | 对象状态的变化 | 封装状态的相关行为 | 单一职责、开闭 |
Iterator | 代码级、应用级 | 对象内部集合的变化 | 封装对象内部集合的使用 | 单一职责 |
Composite | 代码级 | 复杂对象接口的统一 | 统一复杂对象的接口 | 里氏替换 |
Proxy | 应用级、构架级 | 对象访问的变化 | 封装对象的访问 | 里氏替换 |
Builder | 代码级 | 对象组建的变化 | 封装对象的组建 | 开闭 |
Bridge | 代码级 | 对象的多维度变化 | 分离接口以及实现 | 开闭 |
Mediator | 应用级、构架级 | 对象交互的变化 | 封装对象间的交互 | 开闭 |
Memento | 代码级 | 状态的辅助保存 | 封装对象状态的变化 | 接口隔离 |
Visitor | 应用级 | 对象操作变化 | 封装对象操作变化 | 开闭 |
Prototype | 应用级 | 实例化的类 | 封装对原型的拷贝 | 依赖倒置 |
Flyweight | 代码级、应用级 | 系统开销的优化 | 封装对象的获取 | |
Chain of Resp. | 应用级、构架级 | 对象的请求过程 | 封装对象的责任范围 | |
Interpreter | 应用级 | 领域问题的变化 | 封装特定领域的变化 |
OOP 设计原则
前置知识 👉 UML:类图、类的关系
OOP 设计原则:
- 设计模式的基础,即设计的依据;
- 对封装、继承和多态,关联和组合关系的充分理解。
① 单一职责
Single Responsibility
尽量降低类的复杂度,一个类只负责一项职责。
- 提高类的可读性、可维护性,降低变更引起的风险;
- 分解类的粒度,高内聚低耦合。
示例:有一个类 A,负责职责 x 和职责 y。
- 问题:当修改职责 1 的代码时,可能对职责 2 的代码造成影响。
- 解决:将类 A 的粒度分解为 A1,A2(即分为两个类)。
② 接口隔离
Interface Segregation
客户端不依赖不需要的接口。
- 类对另一个类的依赖应建立在最小的接口上。
- 为各个类建立它们需要的专门接口。
示例:有一个接口 F,拥有实现类 F1 和 F2。
有一个类 A,需要使用 F1 的方法;有一个类 B,需要使用 F2 的方法。
(F1 实现了方法 m1, m2, m3,F2 实现了方法 m1, m4, m5。
-
问题:接口 F 对于类 A 和类 B 来说不是最小接口,因为其中包含了它们不需要的方法。
-
解决:将接口 F 拆分为独立的 3 个接口,类 A 和类 B 分别与需要的接口建立依赖关系。
③ 合成复用
Composite Reuse
优先使用聚合/组合关系,其次考虑继承关系。
④ 里式替换
Liskov Substitution
引用父类的地方,必须能透明地使用其子类的对象。
(继承必须确保父类的性质在子类中成立)
继承存在的问题:
- 破坏封装
- 耦合性
- 子类重写父类方法可能导致的影响:
- 降低继承体系的复用性。
- 影响多态的使用。
⑤ 依赖倒置
Dependence Inversion
尽量依赖抽象类,而不依赖具体类(面向抽象编程)。
- 变量的声明类型尽量是抽象类或接口。
- 注意:
- 低层模块最好有抽象类或接口。
- 高层模块不依赖低层模块,二者都应依赖低层模块的抽象。
- 抽象不依赖于细节,细节应依赖于抽象。
- 继承需要遵循里式替换原则
- 常见注入方式:Spring IOC 使用构造器注入或 setter 注入。
- 构造器:定义成员变量,构造器初始化。
- setter:定义成员变量,调用 setter 初始化。
- 方法调用时:定义方法形参列表,调用方法时参数绑定。
示例:
Person 类是高层模块,需要实现接收不同消息的功能。
Email 类是低层模块,需要实现接收电子邮件消息的功能。
-
问题:若增加需求(接收微信、QQ消息),需要新增对应的类和 Person 中对应接收方法;
-
解决:
- 声明接口 MessageReceiver,低层模块(Email、WeChat、QQ)实现该接口,则Person类只需与 MessageReceiver 建立依赖关系即可;
- 此时高层模块(Person)和低层模块(Email、WeChat、QQ),都依赖低层模块的抽象(MessageReceiver)。
⑥ 迪米特
Demeter
(aka 最少知道原则)一个类对自己依赖的类知道的越少越好
- 被依赖的类:代码逻辑都封装在类内部,对外只提供 public 方法。
- 简单理解:只与直接朋友交流。
- 类的依赖关系:成员变量(聚合 / 组合)、方法参数、方法返回值、局部变量
- 直接朋友:成员变量(聚合 / 组合)、方法参数、方法返回值;
- 陌生朋友:局部变量;
示例:
已知 Student 和 Teacher 类,需要分别定义管理类。
StudentManage 能够获取所有学生信息,TeacherManage 能够获取所有老师和学生信息。
-
问题:TeacherManage类中的 “输出所有学生”方法,使用到了成员变量Student(陌生朋友),违背迪米特法则;
-
解决:
-
StudentManage 类中提供输出学生信息的方法。
-
TeacherManage 类中通过方法参数调用 StudentManage 提供的方法。
-
⑦ 开闭(❗)
Open Close
对扩展开放,对修改关闭(扩展 - 增加新的类,修改 - 修改原有代码)
最重要的设计原则,是所有设计原则和设计模式的最终目标。
- 用抽象构建框架,用实现扩展实现。
- 当需要变化时,通过扩展软件实体的行为,而不是通过修改已有代码。
示例:
已知 GraphEditor 类用于绘制图形,还有一些已知的图形类。
-
问题:当新增图形需求时,需要增加 Shape子类、在GraphEditor 类中增加判断分支、增加绘图方法。
-
解决:
-
将 Shape类声明为抽象类,声明一个抽象方法用于绘图(原本在GraphEditor类中的 “绘图方法”)。
-
子类实现该方法。而 GraphEditor类只需通过多态调用方法即可,无需通过分支判断 id。
-