《设计模式》课程第6讲 — Pattern 1(五大结构型与行为型模式 + OO概念反思)
北京大学软件研究所 王亚沙 主讲,0C107 设计模式课程,PPT 导出 PDF,85 页。
课程定位
本讲是设计模式课程的核心 Pattern 1 章节,系统讲解 5 个经典设计模式:Façade(外观)、Adapter(适配器)、Strategy(策略)、Bridge(桥接)、Abstract Factory(抽象工厂)。每个模式均以”问题情境 → 模式图示 → 代码示例 → 关键特征表”四步法展开,辅以生活案例(照相机夜间模式、蜡笔与毛笔、电子商务税务、动物世界等),最后回归面向对象基本概念的深度反思。
一、Façade 外观模式
解决的问题
现代软件系统越来越复杂,设计师常以”分而治之”处理复杂性,将系统划分为多个子系统、子系统又包含多个模块。当客户只关注系统特定功能时,却需要同时与内部诸多模块交互:
- 用户使用系统难度增加(必须了解系统内部结构)
- 客户端代码充满底层模块访问,逻辑复杂
- 维护成本增加(客户端与内部模块紧耦合,内部结构一变就要改客户端)
生活案例:夜间拍合影
- 问题:夜间包厢光线不足,拍好照片需要调焦距、光圈、开闪光灯等,摄影新手来不及学
- 解决方案:相机的”夜间肖像模式”——一键配置好所有底层参数
- “拍摄模式控制模块”为用户提供了简单接口,大大简化了复杂交互关系
模式结构
Client1 Client2 Client3
↓
Façade 类
↙ ↓ ↘
子系统1 子系统2 子系统3
子系统4
代码示例(照相机 Façade)
class Facade {
public:
void setNightMode() {
m_flashlight.open();
m_focus.setFocusValue(5.0);
m_aperture.setApertureValue(3.0);
}
void action() {
m_shutter.action();
}
private:
FlashLight m_flashlight;
Focus m_focus;
Aperture m_aperture;
Shutter m_shutter;
};关键特征
| 维度 | 说明 |
|---|---|
| 意图 | 希望简化原有系统的使用方式,需要定义自己的接口 |
| 问题 | 只需要使用某个复杂系统的子集,或者需要以特殊方式与系统交互 |
| 解 | Façade 为原有系统的 Client 提供一个新的接口 |
| 参与者与协作者 | Façade 接口为客户提供简化的功能调用方式,并调用子系统实现接口承诺的功能 |
| 效果 | 简化对所需子系统的使用过程;但 Façade 不完备,客户可能无法使用某些功能 |
| 实现 | 定义一个(或多个)具备所需接口的新类;让新的类使用原有的系统 |
二、Adapter 适配器模式
解决的问题
有些类我们希望复用,但它的接口不符合我们期望的接口,而期望的接口通常不能修改。
“将一个类的接口转换成客户希望的另一个接口。Adapter 模式使原来由于接口不兼容而不能一起工作的类可以一起工作。“
两种形式
- 对象 Adapter(组合方式,更常用、更灵活)
- 类 Adapter(多重继承方式)
案例:画图系统中的 Circle
- 客户通过 Shape 接口操作所有图形(Point、Line、Square 都实现了 Shape)
- 新增 Circle 形状:可以重新写(工作量大),也可以复用已有的类但接口不符
- 用 Adapter 模式包装已有 Circle 类,使其符合 Shape 接口
关键特征
| 维度 | 说明 |
|---|---|
| 意图 | 将一个你难以控制(无法修改内部代码)的对象匹配到特定接口上 |
| 问题 | 某系统拥有合适的数据和行为,但接口并不合要求 |
| 解 | Adapter 面向所需的接口提供一个包装器 |
| 参与者与协作者 | Adapter 对 Adaptee 进行适配,使其满足 Adapter’s Target 的要求 |
| 效果 | 使先前存在的对象可以匹配新的类型,不受原有接口限制 |
| 实现 | 用一个满足现有接口需求的新类包含已有类,调用已有类的方法 |
Façade vs Adapter 深度对比
| 比较项 | Façade | Adapter |
|---|---|---|
| 是否存在既有的类? | 是 | 是 |
| 是否必须按照某一个接口设计? | 否 | 是 |
| 对象需要多态行为吗? | 否 | 是 |
| 需要更简单的接口吗? | 是 | 否 |
结论:Façade 简化了接口,Adapter 将一个已有接口转换成另一个。
进一步思考:模型与模式的区别
- 设计模型是对设计方案的抽象,抽象的是问题的解
- 设计模式是对在某一特定语境下反复出现的一类问题的解决方案
- 设计模式至少包括 context / problem / solution 三个部分,是一个三元组
“设计模型是鱼,设计模式是渔 —— 授人一鱼不如授人以渔”
三、Strategy 策略模式
解决的问题
一个问题有多种可选策略,概念上功能相同但适用于不同环境。若简单用继承建模策略差异,会导致:
- 类的个数迅速失控
- 代码大量重复、冗余
- 复用无法进行
案例:电子商务税务计算
- SalesOrder 有 calcTax()、printOrder()、GUI() 三个变化维度
- 税:美国 / 加拿大 / 中国(3 种)
- 打印:大单 / 小单 / 固定票据(3 种)
- GUI:Browser / Client(2 种)
若用继承:3 × 3 × 2 = 18 个类,类爆炸。
模式结构
将算法的实现和选择相分离,Context 持有 Strategy 抽象接口,各个 ConcreteStrategy 实现具体算法。
class CalcTax { // Strategy 抽象
public:
virtual double taxAmount(long itemSold, double price) = 0;
};
class CanTax : public CalcTax { // 具体策略
public:
double taxAmount(long itemSold, double price) { return 0.0; }
};
// UsTax 等类似关键特征
| 维度 | 说明 |
|---|---|
| 意图 | 使你可以根据不同的语境灵活配置业务规则或算法 |
| 问题 | 算法选择依赖于用户请求或数据;若算法始终不变则无需 Strategy |
| 解 | 将算法的实现和选择相分离,允许根据语境选择算法 |
| 参与者与协作者 | Strategy 定义算法使用方式;ConcreteStrategies 实现具体算法;Context 通过 Strategy 引用使用特定策略 |
| 效果 | 定义了一族算法;减少 Switch 和条件分支;所有算法必须用同样接口调用 |
| 实现 | Context 包含抽象 Strategy 类;每个派生类实现一个算法;选择算法的责任通常由 Client 承担 |
课堂练习:购物车折扣策略
教材类图书每本减 1 元、连环画 7% 折扣、非教材计算机图书 3% 折扣、其余无折扣。
- 劣解:用 Book 子类(TeachingMaterial / ComicBook / ComputerBook / OtherBook)各自实现 price()
- 优解:用 Strategy 模式,CalPrice 抽象 + MinusOneYuan / Discount7Perc / Discount3Perc / NoDiscount 四个策略
四、Bridge 桥接模式
经典引喻:蜡笔与毛笔
- 3 种粗细 × 12 种颜色 = 36 支蜡笔(乘法关系)
- 3 支毛笔 + 12 种颜料 = 15 个对象(加法关系)
毛笔方案更优雅:
- 对象数目减少(乘法变加法)
- 12 种颜料被 3 种毛笔复用
本质:抽象(abstraction)与实现(implementation)分离。
- 颜料是底层实现(直接在画纸上染色)
- 画笔是高层抽象(画家操作的对象,调用颜料功能但隐藏细节)
意图
“将抽象与其实现解耦,使它们可以独立变化。“
案例:日志工具
- 日志记录方式:DatabaseLog / FileLog
- 运行平台:.NET / Java
不用 Bridge 的问题:
- 违背单一职责原则(一个类有两个变化原因)
- 重复代码多
- 类结构爆炸,扩展性差
模式结构
Abstraction (Log)
↙ ↘
DatabaseLog TextFileLog
↘ ↙
Implementor (ImpLog)
↙ ↘
NImpLog JImpLog
(.NET) (Java)
关键特征
| 维度 | 说明 |
|---|---|
| 意图 | 将一组实现与另一组使用它们的对象分离 |
| 问题 | 抽象类的派生类必须使用多个实现,但不能出现类数量爆炸 |
| 解 | 为所有实现定义一个接口,供抽象类的所有派生类使用 |
| 参与者与协作者 | Abstraction 定义要实现对象的接口;Implementor 为具体实现类定义接口;Abstraction 派生类使用 Implementor 派生类,无需知道具体是哪个 |
| 效果 | 实现与使用实现的对象解耦,提供可扩展性,客户无需操心实现问题 |
| 实现 | 将实现封装在抽象类中;在抽象基类中包含一个实现的句柄 |
Bridge vs Strategy 异同
相似之处:
- 都存在对象以聚合方式引用另一个对象的抽象接口
- 都实现了调用者与被调用者的解耦
- 都是对变化的封装
- 有人说:Strategy 是缩减版的 Bridge
不同之处:
| 维度 | Bridge | Strategy |
|---|---|---|
| 形式 | 两边都可变(Abstraction 和 Implementor 都有继承体系),独立变化 | 只有算法可替换,不考虑 Context 变化 |
| 语意 | Implementor 提供基本操作,Abstraction 基于其定义更高层次操作 | Strategy 提供的是算法,Context 简单调用 |
| 粒度 | 接口更完整,反映结构型模式特点(组合形成更大结构) | 接口仅为协作接口,不涉及其他功能 |
总结: Bridge 表达的是接口隔离原则——把本质上并不内聚的两种体系区别开来,使它们可以松散组合。Strategy 仅在算法层次解耦,没有到体系层次。Bridge 结构中天然包含 Strategy 模式(Abstraction 与 Implementor 的关系),但 Implementor 是成体系的操作,且有状态和数据。
五、Abstract Factory 抽象工厂模式
核心理念:关注点分离
将对象的创建与对象的使用分离。
案例1:图形显示驱动
根据硬件配置选择不同分辨率的显示/打印驱动(低配置 LRDD+LRPD / 高配置 HRDD+HRPD)。
问题: 在 Client 代码中直接 new 具体驱动类(switch-case 创建对象),创建和使用混在一起,且 Client 依赖具体类型。
演进过程
- 无工厂: ApControl 构造函数中 switch-case 直接 new,创建与使用耦合
- 简单工厂: 抽出 CreateHRD / CreateLRD 工厂类,创建与使用分离但 Client 仍依赖具体工厂类名
- 抽象工厂: 引入 AbstractFactory 抽象基类,Client 只依赖抽象接口,隐藏创建具体类的信息
案例2:动物世界(更直观)
- 美洲大陆:美洲虎(食肉)+ 美洲羊(食草)
- 非洲大陆:非洲虎(食肉)+ 非洲羊(食草)
不用 Abstract Factory 的问题:
- 系统依赖对象创建细节(代码中散布 “new 美洲虎”),改动成本高
- 无法从设计上对”产品族”进行约束——可能出现”美洲虎 vs 非洲羊”这种关公战秦琼的错误组合
模式结构
AbstractFactory
+CreateHerbivore()
+CreateCarnivore()
↙ ↘
AmericanFactory AfricanFactory
关键特征
| 维度 | 说明 |
|---|---|
| 意图 | 需要为特定的客户(或情况)提供对象组 |
| 问题 | 需要实例化一组相关的对象 |
| 解 | 协调对象组的创建,将对象实例化规则从使用对象的客户中提取出来 |
| 参与者与协作者 | AbstractFactory 定义创建对象组每个成员的接口;ConcreteFactory 创建对应的具体对象组 |
| 效果 | 将”使用哪些对象”的规则与”如何使用这些对象”的逻辑分离开来 |
| 实现 | 定义抽象类指定创建哪些对象;为每个组实现一个具体类 |
课堂练习:跨平台 UI 构件
Windows / Unix 双平台,每个平台有 Button + Text 构件。要求:
- 新增操作系统(如 Solaris)时符合开闭原则
- 约束同一时刻只使用同一 OS 的构件(不会 WindowsButton + UnixText 混用)
六、面向对象概念的反思
“不是概念的新解,而是回归。”
核心观点:面向对象方法中原本深刻的见解,在传播过程中趋向于表面化。本讲通过 5 个模式的实践,还原一些被”表面化理解”的 OO 概念的本来面目。
对象(Object)
通常的理解(表面化):
- 对象是”数据 + 函数”——“智能数据”
- 直接进入编码细节
正解:
- 对象是具有责任的实体
- 开发中识别的对象可以仅仅是一个概念,只需指出它的责任
- 责任通常表现为:能接收什么消息、具有什么功能,最终表现为功能接口
- 从接口开始构思程序,便于建立倒置的依赖,便于建立灵活结构
“对象只有到了最后实现阶段才需要真正阐明其构成的数据和函数,而在早期(需求、设计、甚至实现早期)我们都不必、也不应该过早陷入细节。“
封装(Encapsulation)
通常的理解(表面化):
- 把数据和操作放到一个类中,使概念相关的在语言层面也相关
- 通过 private/protected/public 控制访问性
正解:
- 信息隐藏——隐藏细节
- 隐藏对象内部细节(private 的数据和函数)
- 隐藏抽象概念的具体实现(通过接口隐藏具体派生类对象)
- 隐藏设计/实现细节(如 Strategy 模式中隐藏具体算法)
五模式关系总览
| 模式 | 分类 | 核心思想 | 关键关键词 |
|---|---|---|---|
| Façade | 结构型 | 简化复杂系统的接口 | 简化接口、子系统封装 |
| Adapter | 结构型 | 将不兼容的接口转换为目标接口 | 接口转换、包装器 |
| Strategy | 行为型 | 分离算法实现与选择,一族算法可互换 | 算法族、可替换策略 |
| Bridge | 结构型 | 抽象与实现解耦,两个维度独立变化 | 双维度变化、接口隔离 |
| Abstract Factory | 创建型 | 封装产品族的创建,保证同组一致性 | 对象族、产品族约束 |
Façade 简化接口 / Adapter 转换接口 / Strategy 替换算法 / Bridge 分离体系 / Abstract Factory 创建对象族
关联笔记
- ss-03-原则-设计模式课程第3讲 — 设计原则(SRP/OCP/LSP/DIP/ISP)
- ss-04-Pattern-1-设计模式课程第4讲 — 同主题(但讲法不同,可对比学习)
- ss-05-消息机制-设计模式课程第5讲 — MFC 消息机制的设计模式视角
- ss-05-比较大的实例-设计模式课程第5讲 — CAD/CAM + 电商订单系统综合案例
- 《设计模式》-GoF-可复用面向对象软件的基础(如已处理)
- 《敏捷软件开发》-原则模式与实践-Robert-C-Martin — 设计原则与模式的实践视角