设计模式课程 第8讲(Pattern 2)——Decorator、Observer、Template Method

北京大学软件研究所 王亚沙 《设计模式》课程第8讲 文字版PDF,44页,pdftotext全文提取约1.6万字符


内容总览

本讲涵盖三大设计模式:

  1. Decorator(装饰器模式)——动态地给一个对象添加职责
  2. Observer(观察者模式)——一对多依赖关系,状态变化自动通知
  3. 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 模式实现
  • 三种模式都体现了「分离变与不变」的设计原则

关联笔记