设计模式课程 第8讲(Pattern 2)——Decorator、Observer、Template Method
北京大学软件研究所 王亚沙 《设计模式》课程第8讲 文字版PDF,44页,pdftotext全文提取约1.6万字符
内容总览
本讲涵盖三大设计模式:
- Decorator(装饰器模式)——动态地给一个对象添加职责
- Observer(观察者模式)——一对多依赖关系,状态变化自动通知
- Template Method(模板方法模式)——定义算法骨架,将步骤推迟到子类
一、Decorator 装饰器模式
解决的问题
场景:已有一些基本功能,可以通过继承扩展功能,但当需要自由组合多种可选功能时,继承会导致类爆炸。
例子:汽车定制
- 基本功能:行驶、转向、停止
- 可选功能(8种):开闭天窗、卫星定位、车窗防弹、车载冰箱、开闭顶棚、8档变速、涡轮加速、自动驾驶
- 定制化需求:用户可以自由组合这些功能
- 例如:卫星定位 + 车载冰箱 + 8档变速 + 自动驾驶
- 或者:开闭天窗 + 卫星定位 + 车窗防弹 + 8档变速 + 涡轮加速 + 自动驾驶
- 问题:用继承的话,组合数量是 2⁸ = 256 种,类爆炸
核心矛盾:我们需要给一个对象而不是整个类添加功能——定制的车不需要量产,不需要为每一种定制写一个类。
模式结构
Component(抽象组件)
└── +Operation()
├── ConcreteComponent(具体组件)
│ └── +Operation() ← 基本功能
│
└── Decorator(抽象装饰器)
├── _component: Component
└── +Operation() { _component->Operation(); } ← 转发调用
├── ConcreteDecoratorA
│ ├── -addedState
│ └── +Operation() { /* 附加功能A */ 父类Operation(); /* 附加功能A */ }
│
└── ConcreteDecoratorB
├── -addedState
└── +Operation() { /* 附加功能B */ 父类Operation(); /* 附加功能B */ }
经典示例:UI 组件装饰
场景:TextView 显示正文,可选功能:滚动条、边框
VisualComponent(接口)
└── +Draw()
└── +Resize()
├── TextView(具体组件)
│ └── +Draw() ← 显示正文
│
└── Decorator(抽象装饰器)
├── _component: VisualComponent
└── +Draw() { _component->Draw(); }
└── +Resize() { _component->Resize(); }
├── BorderDecorator
│ ├── -_width: int
│ └── +Draw() { Decorator::Draw(); DrawBorder(_width); }
│
└── ScrollDecorator
├── -scrollPosition
└── +Draw() { /* 滚动处理 */ Decorator::Draw(); }
使用方式:
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 被装饰,Decorator 添加功能,Component 定义统一接口 |
| 效果 | 功能放在小对象中,可动态组合,对象链终于 ConcreteComponent |
| 实现 | 抽象装饰类继承组件接口,持有组件指针,将新功能调用放在对下一个对象调用的前后 |
课堂练习:发票打印
问题:发票有正文,可选 3 种表头 + 2 种页脚,用户可以自由组合(有/无表头,1/2/3个表头,有/无页脚,1/2个页脚)
解决方案:
- Component:
Ticket接口,prtTicket()方法 - ConcreteComponent:
SalesTicket(正文) - Decorator: 抽象装饰器,持有 Component
- ConcreteDecorator:
Header1/Header2/Header3/Footer1/Footer2 - 每个装饰器的
prtTicket():先调用父类(打印前面的内容),再打印自己的表头/页脚
二、Observer 观察者模式
解决的问题
场景:一个对象状态的改变会导致另外一些对象状态变化——一对多的依赖关系。
经典例子:MFC 的 Doc-View 结构
- 数据存在 Doc 对象中
- 展现由 View 对象负责
- Doc 变化时,所有关联的 View 自动刷新
电商系统例子:新用户注册时
- 需要发送欢迎邮件
- 需要验证地址
- 未来可能还要加:播放欢迎音频、赠送积分、发送短信……
反模式:紧耦合设计
方案一:Customer 类里直接写两个方法
class Customer {
void welcomeLetter(); // 私有
void addrVerification(); // 私有
public:
void addCustomer() {
// ... 其他操作
welcomeLetter();
addrVerification();
}
};问题:僵化(难以修改)、牢固(难以复用)
方案二:提取独立的类
Customer + WelcomLetter + AddrVerification
问题:
- 仍然依赖具体类(而非抽象)
- addCustomer 必须知道要通知哪些对象——紧耦合
- 新增需求(如播放音频)需要修改 Customer 类——僵化
模式思想
- 将对象分为两类:Subject(主体/被观察者) 和 Observer(观察者)
- 一对多关系
- 关系在运行时动态建立:Observer 向 Subject 注册,Subject 事件发生时 Notify 所有 Observer
模式结构
Subject(抽象主体)
├── +Attach(Observer*)
├── +Detach(Observer*)
└── +Notify()
│ 遍历 _observers 列表,调用每个的 Update()
│
└── _observers: List<Observer*>
Observer(抽象观察者)
└── +Update(Subject*)
ConcreteSubject(具体主体,如 Customer)
├── -customerInfo
├── +getCustomerInfo()
└── +setCustomerInfo() // 内部调用 Notify()
ConcreteObserver(具体观察者,如 WelcomeLetter、AddrVerification)
└── +Update(Subject* theChangedSubject) {
// 从 theChangedSubject 获取数据
// 执行自己的逻辑
}
与 Adapter 模式配合
问题:WelcomLetter 和 AddrVerification 是现成的类,接口不一致(没有 Update 方法)
解决:用 Adapter 模式包装
Subject ←→ Observer ←→ WLAdapter → WelcomLetter
←→ AVAdapter → AddrVerification
经典示例:计时器 + 双时钟
场景:一个无界面的计时器 ClockTimer,驱动数字时钟和模拟时钟两个界面
// Subject 基类
class Subject {
virtual void Attach(Observer* o) { _observers->Append(o); }
virtual void Detach(Observer* o) { _observers->Remove(o); }
virtual void Notify() {
for (每个 observer) observer->Update(this);
}
private:
List<Observer*>* _observers;
};
// 具体 Subject:计时器
class ClockTimer : public Subject {
public:
int GetHour();
int GetMinute();
int GetSecond();
void Tick() { /* 更新时间 */ Notify(); }
};
// 具体 Observer:数字时钟
class DigitalClock : public Widget, public Observer {
public:
DigitalClock(ClockTimer* s) : _subject(s) { _subject->Attach(this); }
~DigitalClock() { _subject->Detach(this); }
void Update(Subject* theChangedSubject) {
if (theChangedSubject == _subject) Draw();
}
void Draw() { /* 从 _subject 获取时分秒并绘制 */ }
private:
ClockTimer* _subject;
};客户端使用:
ClockTimer* timer = new ClockTimer;
AnalogClock* analogClock = new AnalogClock(timer);
DigitalClock* digitalClock = new DigitalClock(timer);
// timer 变化时,两个时钟自动更新关键特征
| 维度 | 说明 |
|---|---|
| 意图 | 定义对象间一对多依赖关系,当一个对象状态改变时,所有依赖它的对象自动得到通知并更新 |
| 解耦 | Subject 不知道具体有哪些 Observer,只知道 Observer 接口 |
| 动态性 | 运行时动态注册/注销观察者 |
| 推模型 vs 拉模型 | 推:Notify 时把数据传过去;拉:Observer 收到通知后自己去 Subject 取(本例用的是拉模型) |
| 与其他模式配合 | 现成类接口不一时,用 Adapter 包装成 Observer |
三、Template Method 模板方法模式
解决的两类问题
问题 1:代码复用问题
场景:框架结构相似,但细节不同
例子:访问两种数据库(Oracle 和 SQL Server)
- 流程相同:连接数据库 → 构造查询语句 → 执行查询
- 但细节不同:连接方法不同、SQL 语法略有不同
- 问题:代码重复度高,如何复用?
问题 2:约束和规则问题
场景:程序中有一些规则/约束希望不被违反
例子:使用临界资源必须先进入临界区,用完后退出
- 约束:进入临界区 → 使用资源 → 退出临界区
- 如果让每个程序员自己保证,容易出错
- 问题:如何让约束一次性编码,处处有效?
模式思想
- 写一个 Template(模板),在其中编码高层策略、规则、流程
- 具体细节通过继承来扩展——子类重定义某些步骤
模式结构
AbstractClass(抽象类)
├── templateMethod() ← 模板方法:定义算法骨架
│ {
│ primitiveOp1(); ← 调用基本操作(可被重写)
│ primitiveOp2();
│ ...
│ }
├── #primitiveOp1() ← 基本操作(抽象/钩子方法)
└── #primitiveOp2()
ConcreteClass(具体子类)
├── #primitiveOp1() ← 实现具体步骤
└── #primitiveOp2()
示例 1:数据库访问
DatabaseAccessor(抽象类)
├── query(sql: string) ← 模板方法
│ {
│ connect(); // 连接数据库
│ buildSQL(sql); // 构造查询(SQL 语法调整)
│ execute(); // 执行查询
│ }
├── #connect() = 0 ← 各 DBMS 不同
├── #buildSQL() = 0 ← SQL 语法不同
└── #execute() = 0 ← 执行接口不同
├── OracleAccessor
│ ├── #connect() { ... }
│ ├── #buildSQL() { ... }
│ └── #execute() { ... }
│
└── SqlServerAccessor
├── #connect() { ... }
├── #buildSQL() { ... }
└── #execute() { ... }
示例 2:View 绘图约束
约束: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 解法:
class Printer {
public:
void print() { // 模板方法,编码临界区约束
beginPrint(); // 进入临界区(加锁)
doPrint(); // 实际打印(子类实现)
endPrint(); // 退出临界区(解锁)
}
protected:
virtual void doPrint() = 0;
private:
void beginPrint(); // 加锁
void endPrint(); // 解锁
};三模式对比与联系
| 模式 | 核心思想 | 适用场景 | 关键对象 |
|---|---|---|---|
| Decorator | 动态给对象加功能 | 功能可自由组合、类爆炸 | Component + Decorator |
| Observer | 一对多通知 | 状态变化需要联动多个对象 | Subject + Observer |
| Template Method | 算法骨架 + 可变步骤 | 流程固定、细节不同;约束编码 | AbstractClass + ConcreteClass |
联系:
- Observer 模式中,已有类接口不一致时,可用 Adapter 包装成 Observer
- Template Method 的各步骤如果独立变化,每个步骤可用 Strategy 模式实现
- 三种模式都体现了「分离变与不变」的设计原则