设计模式课程 第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 // 纯虚,子类实现具体打印
六、工厂模式概述
为什么需要工厂
什么是工厂:工厂就是生产其他对象的对象或类,有时还负责管理生产出来的对象。
为什么需要工厂——关注点分离:
- 决定对象是什么,以及它们之间如何协作
- 在各种情况下实例化正确的对象
将”创建哪些对象、用什么参数创建、如何管理对象池”与”如何使用对象”分离开。
规则:对象应该要么构造和/或管理其他对象,要么使用对象,而不应该兼而有之。
与其他笔记的关联
- 第3讲:设计原则(SRP/OCP/LSP/DIP/ISP)
- 第4讲:Pattern 1
- 第5讲:消息机制
- 第6讲:比较大的实例
- 第7讲:比较大的实例(二)
- 第8讲:Pattern 2
关键信息提取说明
- PDF类型:文字版(PPT导出)
- 提取方式:pdftotext 全文提取
- 特殊说明:本讲内容提要标注为”Factory Method + Prototype + Singleton + 多种工厂比较”,但正文实际包含 Decorator/Observer/Template Method 内容(与第8讲有重叠),可能是课件合并或排版问题。本笔记以正文实际内容为准。
- 建议与第8讲笔记对照阅读