设计模式课程 第9讲(Pattern 3)—— 工厂类模式

来源:北京大学软件研究所 王亚沙《设计模式》课程
文件:田浩然上传的资料/0C107设计模式/ss-09(Pattern 3).pdf
处理日期:2026-08-28

课程信息

  • 课程编号:0C107 设计模式
  • 讲次:第9讲(Pattern 3,创建型模式)
  • 主题:各种各样的工厂(Factory Method、Prototype、Singleton 等创建型模式)
  • 讲师:王亚沙,北京大学软件研究所
  • 页数:约49页

注意:本PDF内容提要标注为”Factory Method + Prototype + Singleton + 多种工厂的比较”四部分,但实际正文内容中还穿插了 Decorator、Observer、Template Method 三种模式的内容(与第8讲有重叠),可能是课件排版或合并问题。本笔记以实际正文内容为准整理。


一、Factory Method 工厂方法模式

解决的问题

两类核心问题:

第一类问题:框架扩展

  • 面向对象的软件框架通常用抽象类维护对象关系(信息隐藏原则)
  • 具体应用如何在不修改框架的前提下扩展功能?
  • 典型例子:MFC的 Application / Document 框架
    • 框架只知道抽象的 Application 和 Document 类
    • 用户基于框架开发绘图应用,需要 DrawingApplication 和 DrawingDocument
    • 当用户选择”新建/打开”菜单时,Application 需要创建具体的 Document
    • 问题:框架编写者无法预见具体应用类,如何构造独立于具体应用的框架?

第二类问题:平行结构关联

  • 两组相关联的对象,一组中的某个对象存在,另一组中的关联对象也应该存在
  • 如何表示这种关联关系?
  • 典型例子:可交互图形系统
    • Figure 系列:LineFigure / TextFigure
    • Manipulator 系列:LineManipulator / TextManipulator
    • 每个 Figure 对应一个 Manipulator,如何表达这种配对关系?

核心思路

将需要创建什么具体类的信息,下放到创建类的子类来实现。从而让创建类与具体被创建类分离。

结构与示例

MFC文档框架示例:

Document (抽象产品)
├─ Open()
├─ Close()
├─ Save()
└─ Revert()

Application (抽象创建者)
├─ CreateDocument()  ← 工厂方法(纯虚函数)
├─ NewDocument()
└─ OpenDocument()

MyApplication (具体创建者)     MyDocument (具体产品)
└─ CreateDocument()  ──────────► 返回 new MyDocument

框架代码调用方式:

Document* doc = CreateDocument();  // 调用工厂方法
docs.Add(doc);
doc->Open();

关键特征

维度内容
意图定义一个用于创建对象的接口,让子类决定实例化哪一个类。将实例化推迟到子类
问题一个类需要实例化另一个类的派生类,但不知道是哪一个。Factory Method 允许派生类进行决策
解派生类对实例化哪个类和如何实例化做出决策
参与者Product(工厂方法创建对象的接口)、Creator(定义工厂方法的接口)
效果客户需要派生 Creator 以创建特定的 ConcreteProduct 对象
实现在抽象类中使用抽象方法(C++纯虚函数),抽象类代码引用此方法,但不知道具体对象是哪一个

二、Prototype 原型模式

核心示例:图形编辑器工具

在图形/音乐编辑器中,工具栏上有各种工具(Tool),每种工具负责创建一种图形元素(Graphic)。如果每种图形都对应一个 Tool 子类,类爆炸。

解决方案:GraphicTool 持有一个原型对象,通过 Clone() 创建新对象。

Graphic (抽象产品)         Tool (抽象工具)
├─ Draw(Position)          └─ Manipulate()
└─ Clone()

Staff (具体工具)           RotateTool (具体工具)
└─ Manipulate()            └─ Manipulate()

GraphicTool (通用工具)     MusicNote (具体产品)
├─ prototype: Graphic*     ├─ Draw()
└─ Manipulate() {          └─ Clone() → 返回自身拷贝
    p = prototype->Clone();
    while(user drags) { p->Draw(new pos); }
    insert p into drawing;
  }

WholeNote (具体产品)
├─ Draw()
└─ Clone() → return copy of self

与 Factory Method 的对比

Prototype 用”克隆原型对象”替代了 Factory Method 的”子类创建新对象”。

  • Factory Method 需要为每种产品创建一个 Creator 子类
  • Prototype 只需要一个通用的 GraphicTool,给它不同的原型对象即可

三、Decorator 装饰模式

说明:本部分内容在正文中出现,但内容提要未标注。可能是与其他讲次合并或排版问题。为完整起见一并整理。详见第8讲笔记。

结构

VisualComponent (Component)
└─ Draw()

TextView (ConcreteComponent)    Decorator (装饰抽象类)
└─ Draw()                        ├─ component: VisualComponent*
                                 └─ Draw() { component->Draw(); }

ScrollDecorator                  BorderDecorator
├─ scrollPosition                ├─ borderWidth
└─ Draw() {                      └─ Draw() {
     Decorator::Draw();               Decorator::Draw();
     ScrollTo();                      DrawBorder(_width);
   }                               }

使用方式

Window* win = new Window;
TextView* tv = new TextView;
 
// 最简单的
win->SetContents(tv);
// 有边框的
win->SetContents(new BorderDecorator(tv, 1));
// 有滚动条的
win->SetContents(new ScrollDecorator(tv));
// 既有边框又有滚动条的(组合装饰)
win->SetContents(new BorderDecorator(new ScrollDecorator(tv), 1));

关键特征

维度内容
意图动态地为一个对象添加职责
问题对象执行基本功能,但可能需要添加其他附加功能,种类和数量不确定
解通过添加装饰类(而非扩展子类),在运行时为基本类对象扩充功能
效果所添加的功能放在小对象中,可以在对象功能之前/之后动态添加。对象链终于 ConcreteComponent
实现创建抽象类表示原类和新功能,装饰类中将新功能调用放在对紧随其后对象的调用之前或之后

课堂练习:发票打印

  • 所有发票有正文部分
  • 可选择添加表头(3种:表头1/2/3)和页脚(2种:页脚1/2)
  • 任意组合:有/无表头,1/2/3个表头,有/无页脚,1/2个页脚
  • 用 Decorator 模式:Component + SalesTicket + Decorator + Header1/2/3 + Footer1/2

四、Observer 观察者模式

说明:本部分内容在正文中出现,但内容提要未标注。详见第8讲笔记。

解决的问题

对象间一对多依赖关系:一个对象状态变化导致其他对象状态变化。

  • 经典例子:MFC 的 Doc-View 结构
    • 数据存在 Doc 对象中,展现由 View 对象负责
    • Doc 变化时,所有关联的 View 自动刷新

反例:电子商务新用户

需求:新用户进入系统时,发送欢迎邮件 + 邮局地址验证

方案一(紧耦合):Customer 类内两个私有方法

  • 问题:僵化(难以修改)、脆弱(难以复用)

方案二(依赖具体类):Customer 调用 WelcomLetter 和 AddrVerification 类

  • 问题:仍然紧耦合,新增需求(如播放欢迎音频)需要修改 Customer 类

Observer 方案:

  • Subject(主体/被观察者)+ Observer(观察者)
  • 一对多关系,运行时动态建立
  • Observer 向 Subject 注册 → Subject 事件发生时 Notify 所有 Observer

结构

Subject (抽象主体)                 Observer (抽象观察者)
├─ Attach(Observer*)               └─ Update()
├─ Detach(Observer*)
└─ Notify() → 遍历_observers列表
                                   调用每个Observer的Update()

Customer (具体主体)                WelcomLetter (具体观察者)
├─ customerInfo                    └─ Update() { 发欢迎信 }
├─ getCustomerInfo()
└─ setCustomerInfo()               AddrVerification (具体观察者)
                                   └─ Update() { 验证地址 }

与 Adapter 配合

如果现成的 WelcomLetter 和 AddrVerification 接口不一致,用 Adapter 模式包装:

  • WLAdapter 继承 Observer,内部持有 WelcomLetter
  • AVAdapter 继承 Observer,内部持有 AddrVerification

代码示例:计时器 + 双时钟

class Subject {
    List<Observer*>* _observers;
public:
    void Attach(Observer* o) { _observers->Append(o); }
    void Detach(Observer* o) { _observers->Remove(o); }
    void Notify() {
        for (ListIterator<Observer*> i(_observers);
             !i.IsDone(); i.Next())
            i.CurrentItem()->Update(this);
    }
};
 
class ClockTimer : public Subject {
    int GetHour(), GetMinute(), GetSecond();
    void Tick() { /* 更新时间 */ Notify(); }
};
 
class DigitalClock : public Widget, public Observer {
    ClockTimer* _subject;
public:
    DigitalClock(ClockTimer* s) : _subject(s) { _subject->Attach(this); }
    ~DigitalClock() { _subject->Detach(this); }
    void Update(Subject* s) { if (s == _subject) Draw(); }
    void Draw() { /* 绘制数字时钟 */ }
};
 
// Client:
ClockTimer* timer = new ClockTimer;
AnalogClock* analogClock = new AnalogClock(timer);
DigitalClock* digitalClock = new DigitalClock(timer);

五、Template Method 模板方法模式

说明:本部分内容在正文中出现,但内容提要未标注。详见第8讲笔记。

解决的两类问题

问题1:复用问题

  • 框架结构相似但细节不同的代码
  • 例子:访问不同 DBMS(Oracle vs SQL Server)
    • 流程:连接数据库 → 构造查询语句 → 执行查询
    • 连接方法和SQL语法不同,但整体流程相同
    • 如何复用代码?

问题2:约束和规则

  • 程序中有些规则/约束希望不被违反
  • 例子:临界资源使用(进入临界区 → 使用 → 退出临界区)
  • 让所有使用方自己保证约束不可靠
  • 如何一次性编码约束,使其处处有效?

核心思路

写一个 Template,在其中编码高层的策略、规则、流程。需要针对具体问题精化时,通过继承扩展 Template 中的细节。

示例

数据库访问:

  • 抽象类定义 Query() 模板方法
  • 子类实现 Connect()、BuildSQL()、Execute() 等步骤

View绘图约束:

class View {
public:
    void Display() {    // 模板方法:约束编码在这里
        setFocus();
        doDisplay();    // 纯虚函数,子类实现
        resetFocus();
    }
protected:
    virtual void doDisplay() = 0;  // 原语操作
};
 
class MyView : public View {
protected:
    void doDisplay() override { /* 绘制具体内容 */ }
};

关键特征

维度内容
意图定义一个操作中算法的骨架,将一些步骤推迟到子类中实现。不改变算法结构即可重新实现某些步骤
问题要完成某一细节层次一致的过程/步骤,但个别步骤在更细节层次上有不同实现
解允许定义可变的子步骤,同时保持基本步骤不变
参与者AbstractClass(含模板方法,调用原语操作)、ConcreteClass(实现原语操作)
效果很好的代码复用平台;约束/规则可一次编码处处使用
实现抽象类 + 抽象方法实现过程。如果步骤独立变化,每个步骤可用 Strategy 模式实现

课堂练习:打印机临界资源

  • 打印机是临界资源,同一时刻只能一个线程操作
  • Template Method 方案:
    Printer
    ├─ print()        // 模板方法:beginPrint() → doPrint() → endPrint()
    ├─ beginPrint()   // 进入临界区
    ├─ endPrint()     // 退出临界区
    └─ doPrint() = 0  // 纯虚,子类实现具体打印
    

六、工厂模式概述

为什么需要工厂

什么是工厂:工厂就是生产其他对象的对象或类,有时还负责管理生产出来的对象。

为什么需要工厂——关注点分离:

  1. 决定对象是什么,以及它们之间如何协作
  2. 在各种情况下实例化正确的对象

将”创建哪些对象、用什么参数创建、如何管理对象池”与”如何使用对象”分离开。

规则:对象应该要么构造和/或管理其他对象,要么使用对象,而不应该兼而有之。


与其他笔记的关联


关键信息提取说明

  • PDF类型:文字版(PPT导出)
  • 提取方式:pdftotext 全文提取
  • 特殊说明:本讲内容提要标注为”Factory Method + Prototype + Singleton + 多种工厂比较”,但正文实际包含 Decorator/Observer/Template Method 内容(与第8讲有重叠),可能是课件合并或排版问题。本笔记以正文实际内容为准。
  • 建议与第8讲笔记对照阅读