设计模式课程-第4讲-Façade-Adapter-Strategy-Bridge-AbstractFactory
讲师:王亚沙(北京大学软件研究所) 课程:0C107 设计模式 讲次:第4讲(Pattern 1) 原始文件:
/田浩然上传的资料/0C107设计模式/ss-04(Pattern 1).pdf
本讲概要
本讲系统介绍5种经典设计模式,并在最后通过模式实践反思面向对象的核心概念。
| 模式 | 类型 | 核心问题 | 关键思想 |
|---|---|---|---|
| Façade 外观模式 | 结构型 | 复杂子系统接口难用 | 提供统一简化接口 |
| Adapter 适配器模式 | 结构型 | 已有类接口不兼容 | 包装类转换接口 |
| Strategy 策略模式 | 行为型 | 多算法可选,继承爆炸 | 算法与使用分离 |
| Bridge 桥接模式 | 结构型 | 多维度变化,类爆炸 | 抽象与实现解耦 |
| Abstract Factory 抽象工厂 | 创建型 | 创建一族相关对象 | 工厂封装对象族创建 |
一、Façade 外观模式
问题场景
现代软件系统越来越复杂,设计师用”分而治之”处理复杂性。当客户只关注系统特定功能时,往往需要同时与多个底层模块交互,导致:
- 用户使用难度增加(必须了解系统内部结构)
- 客户端代码充满对底层模块的访问
- 维护成本增加(客户端与内部模块紧耦合)
生活隐喻:相机的”夜间肖像模式”
新手拍合影不需要手动调焦距、光圈、闪光灯——只需选”夜间肖像模式”,相机自动配置好所有参数。这就是 Façade:对外提供简单接口,内部封装复杂交互。
模式结构
Client → Façade类 → 子系统1 / 子系统2 / 子系统3 / 子系统4
相机示例 UML
- Client → Façade
- Façade 内部持有:FlashLight(闪光灯)、Focus(焦距)、Aperture(光圈)、Shutter(快门)
- setNightMode() 一次性配置所有子系统
关键特征
- 意图:简化原有系统的使用方式,定义自己的接口
- 问题:只需要使用复杂系统的子集,或以特殊方式交互
- 解:为 Client 提供一个新的简化接口
- 效果:简化使用过程,但客户可能无法使用某些底层功能(Façade 不完整)
- 实现:定义一个(或多个)具备所需接口的新类,让新类使用原有系统
二、Adapter 适配器模式
问题场景
“总是存在一些类,我们希望复用它,但是它却没有我们期望的接口——而且期望的接口通常不能修改。”
定义:将一个类的接口转换成客户希望的另一个接口。Adapter 模式使原本由于接口不兼容而不能一起工作的类可以一起工作。
两种形式
- 对象 Adapter(更常用):Adapter 持有 Adaptee 对象(组合)
- 类 Adapter:Adapter 同时继承 Target 和 Adaptee(多重继承)
示例:图形系统中的 Circle
- 客户通过 Shape 接口操作所有图形(display/undisplay/fill)
- 已有一个第三方 Circle 类功能完整但接口不匹配
- 用 Adapter 包装第三方 Circle,使其符合 Shape 接口
关键特征
- 意图:将难以控制(无法修改)的对象匹配到特定接口
- 问题:系统拥有合适的数据和行为,但接口不合要求
- 解:面向所需接口提供一个包装器
- 效果:使先前存在的对象可以匹配新类型,不受原有接口限制
Façade vs Adapter 辨析
两者都是”包装模式”,结构相似,但意图完全不同:
| 维度 | Façade | Adapter |
|---|---|---|
| 是否必须按某接口设计 | 否 | 是 |
| 对象需要多态行为吗 | 否 | 是 |
| 需要更简单的接口吗 | 是 | 否 |
| 是否存在既有的类 | 是 | 是 |
结论:Façade 简化接口,Adapter 转换接口。
模型 vs 模式的区别
- 设计模型:对设计方案的抽象,抽象的是问题的解
- 设计模式:对特定语境下反复出现的一类问题的解决方案
- 设计模式是三元组:context(语境)/ problem(问题)/ solution(解),三者是整体
“设计模型是鱼,设计模式是渔——授人一鱼不如授人以渔”
三、Strategy 策略模式
问题场景
一个问题有多种可选策略,概念上功能相同但适用不同环境。 若用继承建模策略差异,会导致:
- 类的个数迅速失控(笛卡尔积爆炸)
- 代码大量重复冗余
- 复用无法进行
示例:电商订单系统
SalesOrder 有三个变化维度:
- 税额计算:美国/加拿大/中国(3种)
- 收据打印:大单/小单/固定票据(3种)
- GUI 输入:Browser/Client(2种)
用继承建模:3 × 3 × 2 = 18 个类 → 类爆炸。
模式结构
Context → Strategy接口 → ConcreteStrategyA / ConcreteStrategyB / ...
将算法的实现和选择相分离,允许根据语境选择算法。
关键特征
- 意图:根据不同语境灵活配置业务规则或算法
- 问题:算法选择依赖于用户请求或数据(若算法永远不变,则没必要用 Strategy)
- 解:算法实现与使用分离
- 参与者:
- Strategy:定义算法接口
- ConcreteStrategies:实现具体算法
- Context:通过 Strategy 引用使用具体算法
- 效果:
- 定义了一族算法
- 减少 switch 和 if-else 条件分支
- 实现要点:
- 所有算法必须用相同接口调用
- Context 包含抽象 Strategy 引用
- 策略选择的责任最初由 Client 承担(根据 Context 信息)
课堂练习:购物车折扣系统
教材类减1元、连环画7%折扣、计算机图书3%折扣、其他无折扣。
错误方案:Book 派生 TeachingMaterial/ComicBook/ComputerBook/OtherBook,各自重写 price() → 折扣策略与图书类型紧耦合。
正确方案(Strategy):CalPrice 策略接口 + 4种具体策略(MinusOneYuan/Discount7Perc/Discount3Perc/NoDiscount),Book 持有 CalPrice 引用 → 策略可独立变化。
四、Bridge 桥接模式
核心隐喻:蜡笔与毛笔
- 蜡笔方案:3种粗细 × 12种颜色 = 36支蜡笔(乘法爆炸)
- 毛笔方案:3支毛笔 + 12种颜料 = 15件(加法复用)
本质区别:笔和颜色能否分离 → 抽象与实现能否分离。
- 颜料 = 底层实现(直接在画纸上染色)
- 画笔 = 高层抽象(画家操作的对象,调用颜料的功能)
- 画家(Client)只需关注如何操作画笔,不必知道颜料如何染色
模式意图
将抽象与其实现解耦,使它们可以独立变化。
示例:日志记录工具
需求:支持数据库记录/文本文件记录 2 种方式 × .NET/Java 2 种平台。
传统继承方案:DatabaseLogNET / DatabaseLogJava / FileLogNET / FileLogJava → 4 个类
问题:
- 违背单一职责原则(一个类有两个变化原因:记录方式 + 平台)
- 重复代码多
- 扩展性差(增加 XML 记录方式 × 增加 BeOS 平台 → 类数继续乘)
Bridge 方案
Abstraction(Log) Implementor(ImpLog)
├─ DatabaseLog ├─ NImpLog(.NET平台)
└─ TextFileLog └─ JImpLog(Java平台)
- Log 基类持有 ImpLog 引用(implementor)
- DatabaseLog.Write() 调用 implementor.WriteInDataBase()
- TextFileLog.Write() 调用 implementor.WriteInFile()
增加 XML 记录:新增 XMLLog 继承 Log → 2 个平台自动支持 增加 BeOS 平台:新增 BEImpLog 继承 ImpLog → 2 种记录方式自动支持
关键特征
- 意图:将一组实现与另一组使用它们的对象分离
- 问题:抽象类派生类必须使用多个实现,但不能出现类数量爆炸
- 解:为所有实现定义接口,供抽象类派生类使用
- 效果:实现与使用解耦,提供可扩展性,客户无需操心实现问题
- 实现:将实现封装在抽象类中,在抽象基类中包含实现句柄
课堂练习:容器设计
容器类型(栈/队列)× 底层实现(数组/链表)
Bridge 结构:
- Abstraction:Container → Stack / Queue
- Implementor:ContainerImp → Array / Chain
- ContainerImp 提供底层操作:AddAtPosition / FetchAtPosition / Count / CurrentIndex
- Stack / Queue 基于底层操作实现高层语义(LIFO / FIFO)
Strategy vs Bridge 辨析
相似之处:
- 都用聚合引用另一个对象的抽象接口
- 都是调用者与被调用者解耦
- 都是对变化的封装
- 有人说 Strategy 是缩减版的 Bridge
不同之处:
| 维度 | Strategy | Bridge |
|---|---|---|
| 形式 | 只有算法可变,Context 不变 | Abstraction 和 Implementor 都可变,且独立变化 |
| 语意 | Strategy 提供的是算法,Context 调用算法 | Implementor 提供基本操作,Abstraction 定义高层操作 |
| 粒度 | 接口仅为协作接口,不涉及其他功能 | 两边都定义完整接口,反映结构型模式特点 |
| 层次 | 算法层次 | 体系层次 |
总结:Bridge 表达的是接口隔离原则——把本质上不内聚的两种体系区别开,使其可以松散组合。Strategy 在解耦上仅到某一个算法的层次。Bridge 结构中必然包含 Strategy 模式(Abstraction 与 Implementor 之间),但 Bridge 的 Implementor 提供一整套成体系操作,且 Abstraction 也可以独立变化。
五、Abstract Factory 抽象工厂模式
关注点分离
将对象的创建与对象的使用分离。
示例:图形显示驱动系统
根据硬件配置选择驱动:低配用低分辨率,高配用高分辨率。
- 显示驱动:LRDD / HRDD
- 打印驱动:LRPD / HRPD
初始方案:ApControl 构造函数中用 switch 创建具体驱动对象 → 对象创建与使用混在一起,Client 依赖具体类型。
演进:引入工厂类专门负责对象生成 → 但 Client 仍依赖具体工厂类(CreateHRD / CreateLRD)。
最终方案:引入抽象工厂基类 CreateRD → Client 只依赖抽象接口,具体工厂类信息对 Client 不可见。
经典示例:动物世界游戏
- 美洲大陆:美洲虎(食肉)+ 美洲羊(食草)
- 非洲大陆:非洲虎(食肉)+ 非洲羊(食草)
直接 new 的问题:
- 当美洲食肉动物由美洲虎改为美洲狮时,所有
new 美洲虎的地方都要改(想象100处分布在30个文件中) - 如果创建后还需要调用 init() 初始化,每个 new 后面都要加
- 无法从设计上约束”只能使用同一大陆的动物”——可能写出
new 美洲虎 + new 非洲羊这种”关公战秦琼”的代码
Abstract Factory 方案
AbstractFactory(接口)
├─ AmericanFactory → AmericanTiger + AmericanSheep
└─ AfricanFactory → AfricanTiger + AfricanSheep
- AbstractFactory 定义 CreateHerbivore() + CreateCarnivore() 两个工厂方法
- 每个 ConcreteFactory 创建一整族相关对象
- Client 只依赖 AbstractFactory 接口,不知道具体创建了哪种动物
关键特征
- 意图:为特定类型的应用创建一个对象族
- 问题:需要实例化一组相关的对象
- 解:协调对象组的创建,将实例化规则从客户对象中提取出来
- 参与者:
- AbstractFactory:定义创建对象组每个成员的接口
- ConcreteFactory:创建对应的具体对象组
- 效果:将”使用哪些对象”的规则与”如何使用这些对象”的逻辑分离开
- 实现:定义抽象类指定创建哪些对象,为每个组实现具体类
课堂练习:跨平台 UI 组件
Windows 风格(Button+Text)vs Unix 风格(Button+Text),要求:
- 增加 Solaris 支持时不必改现有代码(开闭原则)
- 约束同系统构件不能混用(不会出现 WindowsButton + UnixText)
典型 Abstract Factory 应用场景。
六、对面向对象相关概念的反思
“不是概念的新解,而是回归——原本深刻的见解在传播中趋向表面化,现在回归本来面目。“
对象(Object)
通常的理解:对象 = 数据 + 函数 = “智能数据”
- 这种理解直接进入编码细节
- 但 OO 开发范型是”返璞归真”——用日常思考方式开发软件
- 我们通常先理解全局概念,再逐步进入细节
- 早期阶段(需求、设计、甚至实现早期)不应过早陷入数据和函数的细节
正解:对象是具有责任的实体
- 识别对象时只需指出其责任
- 责任表现为:能接收什么消息、具有什么功能 → 最终表现为功能接口
- 从接口开始构思程序,便于建立倒置的依赖,便于灵活结构
封装(Encapsulation)
通常的理解:
- 将数据和操作放到一个类中,使概念相关的在语言层面也相关
- 通过访问控制符(private/protected/public)控制访问性
正解:信息隐藏——隐藏细节
- 隐藏对象内部细节(private 的数据和函数)
- 隐藏抽象概念的具体实现(通过接口隐藏具体派生类对象)
- 隐藏设计/实现细节(如 Strategy 模式中隐藏具体算法实现)
关键洞见汇总
- 模式 = 语境 + 问题 + 解,三者不可分割
- Façade 简化接口,Adapter 转换接口——都是包装但意图不同
- Strategy 是解耦算法,Bridge 是解耦体系——Strategy 是 Bridge 的子集/缩减版
- Abstract Factory 管理对象族——不仅封装创建,还保证族内一致性
- 对象的本质是责任,不是数据+函数——从接口而非数据开始设计
- 封装的本质是信息隐藏——不只是访问控制,更是实现细节的隐藏
关联笔记
- 设计模式课程-第3讲-设计原则(ss-03 原则,先修课程)
- 《设计模式》-可复用面向对象软件的基础-GoF(经典原著)
- 《敏捷软件开发》-原则模式与实践-Robert-C-Martin(SOLID原则+模式实践)
- 《实现模式》-Implementation Patterns-Kent-Beck(实现层模式)