设计模式课程-第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 模式使原本由于接口不兼容而不能一起工作的类可以一起工作。

两种形式

  1. 对象 Adapter(更常用):Adapter 持有 Adaptee 对象(组合)
  2. 类 Adapter:Adapter 同时继承 Target 和 Adaptee(多重继承)

示例:图形系统中的 Circle

  • 客户通过 Shape 接口操作所有图形(display/undisplay/fill)
  • 已有一个第三方 Circle 类功能完整但接口不匹配
  • 用 Adapter 包装第三方 Circle,使其符合 Shape 接口

关键特征

  • 意图:将难以控制(无法修改)的对象匹配到特定接口
  • 问题:系统拥有合适的数据和行为,但接口不合要求
  • 解:面向所需接口提供一个包装器
  • 效果:使先前存在的对象可以匹配新类型,不受原有接口限制

Façade vs Adapter 辨析

两者都是”包装模式”,结构相似,但意图完全不同:

维度FaçadeAdapter
是否必须按某接口设计否是
对象需要多态行为吗否是
需要更简单的接口吗是否
是否存在既有的类是是

结论: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

不同之处:

维度StrategyBridge
形式只有算法可变,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 的问题:

  1. 当美洲食肉动物由美洲虎改为美洲狮时,所有 new 美洲虎 的地方都要改(想象100处分布在30个文件中)
  2. 如果创建后还需要调用 init() 初始化,每个 new 后面都要加
  3. 无法从设计上约束”只能使用同一大陆的动物”——可能写出 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 模式中隐藏具体算法实现)

关键洞见汇总

  1. 模式 = 语境 + 问题 + 解,三者不可分割
  2. Façade 简化接口,Adapter 转换接口——都是包装但意图不同
  3. Strategy 是解耦算法,Bridge 是解耦体系——Strategy 是 Bridge 的子集/缩减版
  4. Abstract Factory 管理对象族——不仅封装创建,还保证族内一致性
  5. 对象的本质是责任,不是数据+函数——从接口而非数据开始设计
  6. 封装的本质是信息隐藏——不只是访问控制,更是实现细节的隐藏

关联笔记