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

两种形式

  1. 对象 Adapter(组合方式,更常用、更灵活)
  2. 类 Adapter(多重继承方式)

案例:画图系统中的 Circle

  • 客户通过 Shape 接口操作所有图形(Point、Line、Square 都实现了 Shape)
  • 新增 Circle 形状:可以重新写(工作量大),也可以复用已有的类但接口不符
  • 用 Adapter 模式包装已有 Circle 类,使其符合 Shape 接口

关键特征

维度说明
意图将一个你难以控制(无法修改内部代码)的对象匹配到特定接口上
问题某系统拥有合适的数据和行为,但接口并不合要求
解Adapter 面向所需的接口提供一个包装器
参与者与协作者Adapter 对 Adaptee 进行适配,使其满足 Adapter’s Target 的要求
效果使先前存在的对象可以匹配新的类型,不受原有接口限制
实现用一个满足现有接口需求的新类包含已有类,调用已有类的方法

Façade vs Adapter 深度对比

比较项FaçadeAdapter
是否存在既有的类?是是
是否必须按照某一个接口设计?否是
对象需要多态行为吗?否是
需要更简单的接口吗?是否

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

不同之处:

维度BridgeStrategy
形式两边都可变(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 依赖具体类型。

演进过程

  1. 无工厂: ApControl 构造函数中 switch-case 直接 new,创建与使用耦合
  2. 简单工厂: 抽出 CreateHRD / CreateLRD 工厂类,创建与使用分离但 Client 仍依赖具体工厂类名
  3. 抽象工厂: 引入 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 控制访问性

正解:

  • 信息隐藏——隐藏细节
    1. 隐藏对象内部细节(private 的数据和函数)
    2. 隐藏抽象概念的具体实现(通过接口隐藏具体派生类对象)
    3. 隐藏设计/实现细节(如 Strategy 模式中隐藏具体算法)

五模式关系总览

模式分类核心思想关键关键词
Façade结构型简化复杂系统的接口简化接口、子系统封装
Adapter结构型将不兼容的接口转换为目标接口接口转换、包装器
Strategy行为型分离算法实现与选择,一族算法可互换算法族、可替换策略
Bridge结构型抽象与实现解耦,两个维度独立变化双维度变化、接口隔离
Abstract Factory创建型封装产品族的创建,保证同组一致性对象族、产品族约束

Façade 简化接口 / Adapter 转换接口 / Strategy 替换算法 / Bridge 分离体系 / Abstract Factory 创建对象族


关联笔记