摘要:设计模式原则、设计模式的划分与简要概括,怎么使用重构获得设计模式并改善代码的坏味道。
本篇作概览与检索用,后续结合源码进行具体模式深入学习。
目录
1、设计模式原理
核心原则(语言无关)
本质原理图
原则关联矩阵
2、设计模式分类
1. 创建型模式
2. 结构型模式
3. 行为型模式
3、重构获得模式
重构关键技法
静态——>动态
早绑定——>晚绑定
继承——>组合
编译时依赖——>运行时依赖
紧耦合——>松耦合
生产设计的原则倾向
什么样的代码算是好的代码呢?同好的服务一样(此处强行关联 《[微服务设计]1_微服务》)——功能正确、高效、可维护性强、健壮抗压、写作流畅。也可以总的来说,低耦合高内聚的高质量可维护代码,就是好的代码。
那么,有没有一些规范可以遵循的呢?
有的,设计模式就是这样的指导规范。
1、设计模式原理
核心原则(语言无关)
众所周知,设计模式有一些原则可以遵守:
单一职责:一个模块/类应仅负责一个职责。降低模块间耦合,提升可维护性。
里氏替换:所有引用基类的地方必须能透明地使用子类对象。这样利于抽象与类型封装。
开放封闭:对修改封闭、对拓展开放。
迪米特:减少对象间不必要的直接交互,降低耦合,提高模块独立性。
接口隔离原则:客户端不应依赖未使用的接口。将大接口拆分为小的、高内聚的接口。
依赖倒置:高层模块不应该依赖于底层模块,转换为二者都依赖于抽象。具体来说就是针对接口编程,而不是针对具体实现,这样可以减少各部分依赖关系,这也面向对象设计的亮点。
还有优先使用对象组合而不是类继承等等……
总的来说:从通用核心原则到实现层原则再到构造策略相关的原则,均追求代码模块的低耦合高内聚,降低复杂度。
本质原理图
原则关联矩阵
原则 | 变化控制 | 认知简化 | 系统弹性 | 典型模式 |
单一职责(SRP) | 高 | 高 | 中 | 外观模式 |
开放封闭(OCP) | 极高 | 中 | 极高 | 策略模式、 装饰器模式 |
里氏替换(LSP) | 中 | 高 | 高 | 组合模式、代理模式 |
接口隔离(ISP) | 高 | 极高 | 中 | 适配器模式、中介模式 |
依赖倒置(DIP) | 极高 | 中 | 高 | 桥接模式、依赖注入模式 |
组合优先 | 高 | 中 | 极高 | 享元模式,职责链模式 |
2、设计模式分类
Gang of Four(四人组,GOF) 在经典著作《设计模式:可复用面向对象软件的基础》中提出的23种设计模式的分类方式,按功能和用途分为三大类——创建型、结构型、行为型。
1. 创建型模式
目的:解决对象的创建问题,封装实例化逻辑,提升代码复用性和灵活性。
核心思想:将对象的创建与使用解耦,通过统一接口或模板控制对象实例化过程。
-
工厂方法 (Factory Method): 定义一个创建对象的接口,但由子类决定要实例化的类。工厂方法将类的实例化推迟到子类。
-
抽象工厂 (Abstract Factory): 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。
-
原型 (Prototype): 通过复制现有对象来创建新对象,而不是通过创建新实例。
-
建造者 (Builder): 用于构建一个复杂对象的表示。使用多个步骤构建该对象,可以将构建过程与其表示分离。
2. 结构型模式
目的:处理类与对象的组合,优化系统结构,提高模块间的解耦性。
关注如何将对象组合成更大的结构,处理类和对象的关系,以实现更大的功能。
核心思想:通过组合、继承或接口,调整类与对象的结构,增强系统的灵活性和扩展性。
-
装饰器 (Decorator): 动态地给对象添加额外的职责或行为,而不影响其他对象的功能。
-
桥接 (Bridge): 将抽象部分与其实现部分分离,使它们可以独立变化。
-
外观 (Facade): 为一组接口提供一个统一的高层接口,使得子系统更易使用。
-
代理 (Proxy): 为其他对象提供一种代理以控制对这个对象的访问。
-
中介者 (Mediator): 用于减少对象之间的通信复杂性,避免对象之间的直接引用。
-
适配器 (Adapter): 在不修改源代码的情况下,允许将不兼容的接口连接起来。
-
享元 (Flyweight): 通过共享对象来支持大量细粒度的对象,减少内存消耗。
3. 行为型模式
目的:定义对象间的通信机制,管理算法或职责的动态变化。
核心思想:通过解耦对象间的直接依赖,实现行为的动态组合、算法替换或事件驱动。
-
模板方法 (Template Method): 在一个方法中定义一个操作的整体结构,而将某些步骤的实现延迟到子类中。
-
策略 (Strategy): 定义一系列算法,将每个算法封装起来,并使它们可以互换,从而使其独立于使用它的客户端。
-
观察者/事件 (Observer/Event): 定义对象间的一对多依赖,确保当一个对象变化时,其依赖对象也会被通知并自动更新。
-
命令 (Command): 将请求封装为对象,从而允许参数化客户、队列请求以及记录请求日志。
-
访问者 (Visitor): 表示一个作用于某种对象结构中的各元素的操作,并使其可以在不改变元素类的前提下定义新的操作。
-
备忘录 (Memento): 捕获一个对象的内部状态,以便在以后恢复对象的状态,而不暴露对象的实现细节。
-
状态 (State): 让一个对象在其内部状态改变时改变其行为,对象将表现得像是它的类发生了改变。
-
组合 (Composite): 将对象组合成树形结构以表示部分-整体的层次,并使客户端对单个对象和组合对象的使用保持一致。
-
迭代器 (Iterator): 提供一种方式访问一个集合对象中的元素,而无需暴露它的内部表示。
-
责任链 (Chain of Responsibility): 将请求的发送者和接收者解耦。将多个对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它。
-
解释器 (Interpreter): 为了一种语言定义一个文法,并提供一个解释器来使用该文法。
3、重构获得模式
需要强调的是:
设计模式是循序渐进的,如同架构设计一样,没有一步到位的设计模式,也少有单一的设计模式,要掌握应用时间、地点。
应对变化,提高复用,寻找变化点。
设计模式应在变化点处应用:设计模式是对动态变化点的设计,在变化、稳定中寻找隔离点,来分离他们,从而管理变化,以稳控变。
没有一个稳定的点,设计模式就没有意义。
设计模式有23种,慢慢理解嘛。
重构关键技法
在《重构-改善既有代码的设计》一书中,作者Martin Fowler有提到一些有效的重构手法,重构时遵守设计原则。
坏味道 | 重构手法 |
重复代码(Duplicated Code) | 提炼函数、参数化、提取类。 |
过长函数(Long Method) | 提炼函数、内联临时变量。 |
过大类(Large Class) | 提炼类、搬移函数/字段。 |
数据泥团(Data Clumps) | 封装为独立类。 |
条件逻辑复杂(Switch/If) | 以多态取代条件、引入策略模式。 |
被拒绝的遗赠 | 函数下移、以委托取代子类。 |
冗余注释(Comments) | 通过代码重构让注释多余(如提炼函数明确意图)。 |
技法对应原则如下:
静态——>动态
更好地适应变化与减少重复代码:动态结构可以通过配置或者接口灵活调整,可以服用逻辑,避免静态编码导致的重复。
设计模式:
如策略模式(通过接口定义算法族,运行时选择具体策略)、观察者模式(事件驱动的动态通知机制)
重构手法:
替换条件分支为多态(如将if-else替换为不同子类实现)。
引入参数化配置(如通过配置文件动态加载行为)。
示例如下:
// 静态:硬编码的条件判断
public void calculateTax(double income) {
if (country == "USA") {
// 美国税率逻辑
} else if (country == "China") {
// 中国税率逻辑
}
}
// 动态:使用策略模式
interface TaxStrategy {
double calculate(double income);
}
class USATax implements TaxStrategy { ... }
class ChinaTax implements TaxStrategy { ... }
public void calculateTax(TaxStrategy strategy, double income) {
return strategy.calculate(income);
}
早绑定——>晚绑定
通过接口和运行时决策增强灵活性。
定义与背景
-
早绑定:在编译时确定对象类型和方法调用(如直接调用具体类的方法)。
-
晚绑定:在运行时动态确定对象类型和方法调用(如通过接口或多态)。
为什么需要转向晚绑定?
-
灵活性:允许在运行时替换实现,支持扩展和插件化。
-
解耦:调用者无需依赖具体实现类,仅依赖抽象接口。
如何实现?
-
面向接口编程:定义接口,由子类实现不同行为。
-
多态与虚函数:通过基类指针/引用调用虚方法。
-
依赖注入:通过外部配置或工厂动态注入对象。
// 早绑定:直接依赖具体类
public void sendEmail(Email email) {
EmailSender sender = new GmailSender();
sender.send(email);
}
// 晚绑定:通过接口实现多态
public interface EmailSender {
void send(Email email);
}
public class GmailSender implements EmailSender { ... }
public class OutlookSender implements EmailSender { ... }
public void sendEmail(Email email, EmailSender sender) {
sender.send(email); // 运行时决定具体实现
}
继承——>组合
用组合替代继承,避免层级过深。
定义与背景
-
继承:通过继承复用父类代码,子类与父类形成“is-a”关系。
-
组合:通过组合其他对象复用功能,形成“has-a”关系。
为什么需要转向组合?
-
减少耦合:继承导致子类与父类高度耦合,组合则通过接口或抽象类解耦。
-
避免继承树复杂化:组合允许动态替换组件,而继承的层级过深易导致维护困难。
如何实现?
-
组合复用:将功能封装为独立对象,通过字段或方法参数组合使用。
-
策略模式:通过组合不同策略对象实现不同行为。
// 继承:硬编码行为
class Animal {
void move() { ... }
}
class Bird extends Animal { ... }
class Fish extends Animal { ... }
// 组合:通过接口解耦
interface MovementStrategy {
void move();
}
class Bird {
private MovementStrategy movement;
public Bird(MovementStrategy movement) {
this.movement = movement;
}
void fly() {
movement.move(); // 组合不同策略
}
}
编译时依赖——>运行时依赖
将依赖关系移到运行时,提高可测试性和扩展性。
定义与背景
-
编译时依赖:依赖关系在代码中硬编码(如直接new具体类)。
-
运行时依赖:依赖关系在运行时动态注入(如通过配置或工厂)。
为什么需要转向运行时依赖?
-
可测试性:便于通过模拟对象(Mock)进行单元测试。
-
灵活性:可替换依赖实现,无需修改代码。
如何实现?
-
依赖注入(DI):通过构造函数、方法或字段注入依赖对象。
-
工厂模式:通过工厂类动态创建对象。
// 编译时依赖:硬编码依赖
class Service {
private Database database = new MySQLDatabase();
void doWork() {
database.query();
}
}
// 运行时依赖:通过构造函数注入
class Service {
private Database database;
public Service(Database database) {
this.database = database; // 运行时注入
}
void doWork() {
database.query();
}
}
紧耦合——>松耦合
通过抽象接口和事件机制降低模块间依赖。
定义与背景
-
紧耦合:类之间直接依赖具体实现,修改一个类可能影响多个类。
-
松耦合:通过抽象接口或事件机制降低依赖,仅依赖抽象。
为什么需要转向松耦合?
-
可维护性:降低修改代码的风险,模块独立。
-
扩展性:新增功能无需修改现有代码(开闭原则)。
如何实现?
-
接口与抽象类:定义抽象接口,类仅依赖接口。
-
观察者模式:通过事件机制解耦对象间的直接通信。
-
发布-订阅模式:对象通过中间事件总线通信。
// 紧耦合:直接调用具体类
class Button {
void onClick() {
Text text = new Text();
text.update();
}
}
// 松耦合:通过接口解耦
interface Updatable {
void update();
}
class Button {
private Updatable listener;
public Button(Updatable listener) {
this.listener = listener;
}
void onClick() {
listener.update();
}
}
总结一下核心思想是动态化、低耦合高内聚、保证可读性与可维护性。
生产设计的原则倾向
低耦合、高内聚是软件设计的核心原则,但并非所有设计场景的最优先考虑的事情。
比如业务服务设计中,优先考虑组织架构所需求的迭代或者用户需求和业务目标,在架构设计中,优先考虑系统性能、可维护性。
需要依据具体场景灵活变动。
再次强调,无必要修改的“完美主义”重构,设计模式也不是最优先考虑的,优先完成,优先保证代码清晰。