设计模式课程第6讲(Pattern 2)- Decorator / Observer / Template Method
北京大学软件研究所 · 王亚沙 · 设计模式 0C107
课程概述
第6讲 Pattern 2 涵盖三个结构型与行为型设计模式:Decorator 装饰模式、Observer 观察者模式、Template Method 模板方法模式。三个模式分别从不同维度解决”扩展与变化”问题:
| 模式 | 核心问题 | 关键机制 | 模式分类 |
|---|---|---|---|
| Decorator | 动态给对象添加职责,避免继承爆炸 | 对象组合 + 递归包装 | 结构型 |
| Observer | 一对多依赖,状态变化自动通知 | 注册-通知机制 + 松耦合 | 行为型 |
| Template Method | 算法骨架不变,步骤细节可变 | 继承 + 抽象方法延迟实现 | 行为型 |
一、Decorator 装饰模式
解决的问题
继承扩展的困境:当需要在基本功能基础上添加多种可选功能,且功能可以自由组合时,继承会导致类爆炸。
- 例:汽车有行驶/转向/停止基本功能
- 扩展方向:豪华房车(天窗/卫星定位/防弹/冰箱)、超炫跑车(顶棚/8档变速/涡轮/自动驾驶)
- 问题:如果用户可以自由选配功能(如卫星定位+车载冰箱+8档变速+自动驾驶),组合数量是指数级的,继承无法应对
核心洞察:我们需要应对的是给一个对象而不是整个类添加功能——定制的轿车不需要量产,不需要为每一款定制靓车编写一个类。
模式结构
Component (接口)
└── Operation()
├── ConcreteComponent ← 基本功能对象
│ └── Operation()
└── Decorator ← 装饰抽象类
├── _component (Component*)
└── Operation() { _component->Operation(); }
├── ConcreteDecoratorA
│ ├── addedState
│ └── Operation() { 前处理 + Decorator::Operation() + 后处理 }
└── ConcreteDecoratorB
├── addedState
└── Operation() { ... }
关键点:Decorator 继承自 Component,同时持有一个 Component 指针——既是 Component 又是 Component 的包装器。
经典示例:UI 组件装饰
场景:TextView 可以显示正文,但有时需要加滚动条、加边框,且可以自由组合。
VisualComponent ← Component
└── Draw() / Resize()
├── TextView ← ConcreteComponent
│ └── Draw()
└── Decorator ← 抽象装饰类
├── _component
└── Draw() { _component->Draw(); }
├── ScrollDecorator ← 滚动条装饰
│ ├── -scrollPosition
│ └── Draw() { Decorator::Draw(); ScrollTo(); }
└── BorderDecorator ← 边框装饰
├── -borderWidth
└── Draw() { Decorator::Draw(); DrawBorder(_width); }
核心代码结构:
class Decorator : public VisualComponent {
VisualComponent* _component;
public:
Decorator(VisualComponent* c) : _component(c) {}
virtual void Draw() { _component->Draw(); }
virtual void Resize() { _component->Resize(); }
};
class BorderDecorator : public Decorator {
int _width;
void DrawBorder(int);
public:
BorderDecorator(VisualComponent* p, int w) : Decorator(p), _width(w) {}
virtual void Draw() {
Decorator::Draw(); // 先调用被装饰对象
DrawBorder(_width); // 再添加自己的功能
}
};使用方式(动态组合):
// 最简单的 TextView
win->SetContents(new TextView);
// 加边框
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 种页脚,用户可自由选择是否添加、添加哪几种。
设计:
Component:prtTicket()接口SalesTicket:ConcreteComponent,打印正文Decorator:持有 Component,prtTicket()转发Header1/Header2/Header3:3 个表头装饰器Footer1/Footer2:2 个页脚装饰器
通过嵌套装饰实现任意组合:new Header1(new Footer2(new SalesTicket))
二、Observer 观察者模式
解决的问题
一对多依赖关系:一个对象状态变化时,需要通知其他多个对象自动更新。
- 典型案例:MFC 的 Doc-View 结构——数据存在 Doc 中,展现由 View 负责;Doc 变化时所有 View 自动刷新
- 电子商务场景:新用户注册时,需要发欢迎邮件 + 地址验证 + 后续可能还有更多操作
反面方案分析
方案一:在 Customer 类里直接写两个方法 welcomeLetter() 和 addrVerification()
- 问题:紧密耦合 → 僵化性(难以修改)+ 牢固性(难以复用)
方案二:将功能拆到独立类中,Customer 调用它们
- 改进:不依赖具体实现,但依赖抽象
- 仍有问题:
- 紧耦合,
addCustomer()必须知道要通知哪些对象 - 僵化,新增需求(如播放欢迎音频)需要修改 Customer 类,重新编译部署
- 紧耦合,
核心洞察:将对象分为主体(subject,被观测目标)和观测者(observer),二者是一对多关系,关系在运行时动态建立。
模式结构
Subject(主体/被观察者) Observer(观察者)
├── Attach(Observer*) └── Update()
├── Detach(Observer*)
├── Notify()
│ └── 遍历所有 observers,调用 Update()
└── _observers: List<Observer*>*
▲
│
Customer (ConcreteSubject)
├── getCustomerInfo()
└── setCustomerInfo() → 状态变化时调用 Notify()
示例:电子商务用户注册
角色分配:
- Subject:Customer 类(新用户注册是事件源)
- Observer:WelcomLetter、AddrVerification(观察者,收到通知后执行各自操作)
接口适配问题:WelcomLetter 和 AddrVerification 是现成类,接口不一致 → 用 Adapter 模式适配
Subject ◄──── Observer
▲
│
WLAdapter AVAdapter
│ │
WelcomLetter AddrVerification
经典示例:计时器 + 双时钟
场景:无界面的计时器 ClockTimer 驱动数字时钟和模拟时钟两种 UI 显示。
// Subject 基类
class Subject {
List<Observer*>* _observers;
public:
void Attach(Observer* o) { _observers->Append(o); }
void Detach(Observer* o) { _observers->Remove(o); }
void Notify() {
ListIterator<Observer*> i(_observers);
for (i.First(); !i.IsDone(); i.Next())
i.CurrentItem()->Update(this);
}
};
// 具体 Subject:计时器
class ClockTimer : public Subject {
public:
int GetHour(); int GetMinute(); int GetSecond();
void Tick() { /* 更新时间 */ Notify(); }
};
// 具体 Observer:数字时钟
class DigitalClock : public Widget, public Observer {
ClockTimer* _subject;
public:
DigitalClock(ClockTimer* s) : _subject(s) { _subject->Attach(this); }
~DigitalClock() { _subject->Detach(this); }
void Update(Subject* theChangedSubject) {
if (theChangedSubject == _subject) Draw();
}
void Draw() { /* 用 _subject->GetHour() 等绘制 */ }
};Client 使用:
ClockTimer* timer = new ClockTimer;
AnalogClock* analog = new AnalogClock(timer);
DigitalClock* digital = new DigitalClock(timer);
// timer.Tick() 时两个时钟自动更新关键特征
| 维度 | 内容 |
|---|---|
| 意图 | 定义对象间一对多依赖,当一个对象状态改变时,所有依赖它的对象自动收到通知并更新 |
| 解耦 | Subject 不知道 Observer 的具体类型和数量,运行时动态注册/注销 |
| 通知方式 | 推模型(Subject 主动推送数据)vs 拉模型(Observer 自己查询) |
| 组合使用 | 常与 Adapter 模式配合(适配现有类的 Observer 接口) |
三、Template Method 模板方法模式
解决的两类问题
问题 1:代码复用
框架结构相似、细节不同的代码如何复用?
- 例:电子商务软件需要支持 Oracle 和 SQL Server 两种 DBMS
- 流程相同:连接数据库 → 构造查询语句 → 执行查询
- 但连接方法、SQL 语法略有不同
- 问题:每种 DBMS 写一个函数,代码重复度高
问题 2:约束和规则
某些规则/约束需要被所有代码遵守,如何一次性编码并保证处处有效?
- 例:使用临界资源前必须进入临界区,使用完必须退出
- 如果让每个使用方自己负责 → 违反”一个规则只实现一次”原则,也不能保证约束不被违背
思路:写一个 Template 编码高层策略、规则、流程;通过继承扩展具体细节。
模式结构
AbstractClass(抽象类)
├── templateMethod() ← 模板方法:定义算法骨架
│ ├── step1(); // 调用基本方法
│ ├── step2();
│ └── step3();
├── # step1() ← 基本方法(抽象/钩子,子类实现)
├── # step2()
└── # step3()
▲
│
ConcreteClass(具体类)
├── # step1() ← 实现具体步骤
├── # step2()
└── # step3()
示例 1:数据库访问
DatabaseAccessor(抽象类)
├── Query(sql) ← 模板方法
│ ├── Connect();
│ ├── BuildStatement(sql);
│ └── Execute();
├── # Connect() ← 子类实现
├── # BuildStatement()
└── # Execute()
├── OracleAccessor ← Oracle 具体实现
└── SqlServerAccessor ← SQL Server 具体实现
示例 2:View 绘图约束
约束:View 必须先获得焦点才能设置图形设备环境(颜色、字体等),才能绘图。
class View {
public:
void Display() { // ← 模板方法,约束编码在这里
setFocus(); // 前置步骤:获得焦点
doDisplay(); // 子类实现具体绘图
resetFocus(); // 后置步骤:释放焦点
}
void setFocus();
void resetFocus();
protected:
virtual void doDisplay() = 0; // ← 基本方法(纯虚)
};
class MyView : public View {
protected:
void doDisplay() override { /* 渲染视图内容 */ }
};关键点:约束(获得焦点→绘图→释放焦点)被编码在 Display() 模板方法中,子类只需关心绘图本身,永远不会违反约束。
关键特征
| 维度 | 内容 |
|---|---|
| 意图 | 定义一个操作中算法的骨架,将一些步骤推迟到子类中实现 |
| 问题 | 过程/步骤在高层一致,但个别步骤在更细节层次上有不同实现 |
| 解 | 允许定义可变的子步骤,同时保持基本步骤不变 |
| 参与者 | AbstractClass(含模板方法 + 基本方法)/ ConcreteClass(实现基本方法) |
| 效果 | 很好的代码复用平台;约束/规则一次编码处处使用 |
| 实现 | 抽象类 + 抽象方法实现过程;如果步骤独立变化,每个步骤可用 Strategy 模式实现 |
课堂练习:打印机临界资源
规则:打印机是临界资源,同一时刻只能有一个线程操作。
Template Method 方案:
Printer(抽象类)
├── print() ← 模板方法,编码临界区约束
│ ├── beginPrint(); // 进入临界区(加锁)
│ ├── doPrint(); // 子类实现具体打印
│ └── endPrint(); // 退出临界区(解锁)
├── -beginPrint()
├── -endPrint()
└── # doPrint() ← 纯虚,子类实现
▲
│
MyPrinter
└── # doPrint()
约束编码在 print() 模板方法中,所有继承 Printer 的子类自动遵守临界区规则。
三模式对比与联系
| 模式 | 扩展方式 | 关系 | 变化点 | 典型场景 |
|---|---|---|---|---|
| Decorator | 对象组合 + 递归包装 | 1对1 链式 | 功能组合 | UI 组件装饰、流过滤器、IO 包装 |
| Observer | 注册-通知 + 松耦合 | 1对多 | 观察者数量和类型 | 事件系统、MVC、数据绑定、发布订阅 |
| Template Method | 继承 + 抽象方法 | 父类→子类 | 算法步骤实现 | 框架骨架、流程约束、代码复用 |
模式组合:
- Observer + Adapter:观察者接口不匹配时用 Adapter 适配
- Template Method + Strategy:如果各步骤独立变化,每个步骤可改用 Strategy 实现
- Decorator + Composite:装饰模式常与组合模式配合使用
本讲要点回顾
- Decorator 解决的是”功能自由组合”问题——用对象组合替代继承扩展,避免类爆炸
- Observer 解决的是”一对多通知”问题——主体和观察者松耦合,运行时动态建立关系
- Template Method 解决的是”算法骨架复用 + 步骤延迟实现”问题——父类定框架,子类填细节
设计原则体现:
- 开闭原则(OCP):三个模式都允许在不修改现有代码的前提下扩展功能
- 依赖倒置原则(DIP):都依赖抽象而非具体
- 单一职责原则(SRP):每个装饰器/观察者/模板步骤各司其职