【读书笔记】《C++ Software Design》第六章深入剖析 Adapter、Observer 和 CRTP 模式
《C++ Software Design》第六章深入剖析 Adapter、Observer 和 CRTP 模式
在现代 C++ 软件设计中,设计模式(Design Patterns) 早已超越传统的“套路”概念,演化为一种构建高扩展性、高抽象性的系统结构指南。《C++ Software Design》一书第六章精炼地介绍了三类实用的设计模式:Adapter、Observer 和 CRTP(Curiously Recurring Template Pattern)。
Guideline 24:使用 Adapter 模式统一接口(Standardize Interfaces)
Adapter 模式的基本原理
Adapter(适配器) 模式的核心目的是将不兼容接口的类转换为用户所期望的接口。你可以把它想象成“接口翻译器”。
UML 示意:
[Client] → [Target Interface] ← [Adapter] ← [Adaptee]
应用场景:
- 旧有系统迁移到新接口标准
- 第三方库的接口无法修改
- 多个接口标准需要统一封装
Object Adapter vs Class Adapter
C++ 中可以通过两种方式实现适配器:
1. Object Adapter(对象适配器)
通过组合方式,将已有类的对象作为成员,并在适配器内部转换调用。
class Adaptee {
public:void specificRequest() {std::cout << "Adaptee called\n";}
};class Target {
public:virtual void request() = 0;
};class Adapter : public Target {Adaptee adaptee;
public:void request() override {adaptee.specificRequest(); // 转换调用}
};
2. Class Adapter(类适配器)
使用多重继承,使适配器既继承目标接口,也继承现有实现类:
class Adapter : public Target, public Adaptee {
public:void request() override {specificRequest(); // 直接调用继承来的方法}
};
缺点:类适配器强依赖继承,侵入性强,不适合多适配组合。
标准库中的 Adapter 示例
std::function
可以适配任意可调用对象(函数指针、lambda、functor)std::back_insert_iterator
是对容器插入操作的适配<functional>
中的std::bind
,std::mem_fn
,std::not1
都是函数适配器
Adapter 与 Strategy 的对比
特性 | Adapter | Strategy |
---|---|---|
目标 | 兼容已有接口 | 提供可替换的算法 |
封装行为 | 不是行为封装,而是接口转换 | 封装行为并可切换 |
应用对象 | 面向接口兼容问题 | 面向策略多样性 |
Adapter 的缺点分析
- 多重适配链容易导致复杂依赖
- 隐藏真实接口,可能造成可读性下降
- 类适配器过度依赖继承(不可组合)
Guideline 25:使用 Observer 实现解耦的通知机制(Abstract Notification)
Observer 模式的基本原理
Observer(观察者)是一种 发布-订阅(Publish-Subscribe)机制,当“主题对象”状态变化时,通知所有观察者对象。
UML 示意:
[Subject] → maintains list of → [Observers]
[Observer] ← notified by ← [Subject::notify()]
关键点:
- 抽象通知机制
- 支持多观察者
- 动态添加/移除观察者
经典实现方式
class Observer {
public:virtual void update(int value) = 0;
};class Subject {std::vector<Observer*> observers;int value;
public:void attach(Observer* o) {observers.push_back(o);}void setValue(int v) {value = v;for (auto* o : observers) {o->update(value);}}
};
值语义的观察者实现(Modern C++ 风格)
使用 std::function
、std::unordered_map
代替原始指针和继承层次:
class Subject {using Callback = std::function<void(int)>;std::unordered_map<int, Callback> observers;int nextId = 0;
public:int subscribe(Callback cb) {int id = nextId++;observers[id] = std::move(cb);return id;}void unsubscribe(int id) {observers.erase(id);}void notify(int val) {for (auto& [_, cb] : observers) cb(val);}
};
Observer 的缺陷
- 如果未正确解除绑定,可能出现 悬挂引用
- 串联通知容易形成 更新风暴
- 缺乏线程安全机制时存在并发问题
Guideline 26:使用 CRTP 实现静态类型特性(Static Type Categories)
CRTP 的动机
Curiously Recurring Template Pattern(奇异递归模板模式) 通过将派生类作为模板参数传递给基类,实现 静态多态(Static Polymorphism)。
template<typename Derived>
class Base {
public:void interface() {static_cast<Derived*>(this)->implementation();}
};class Derived : public Base<Derived> {
public:void implementation() {std::cout << "Derived logic\n";}
};
使用场景
- 静态分发函数行为
- 消除虚函数开销
- 编译期强类型行为(如标签分类)
CRTP 的缺点分析
- 可读性差:对新手不友好,调试困难
- 耦合性强:派生类必须符合模板要求,不能动态切换行为
- 模板膨胀:容易造成代码 bloat
CRTP vs C++20 Concepts
特性 | CRTP | Concepts |
---|---|---|
类型约束 | 编译期类型匹配 | 明确声明概念接口 |
抽象性 | 依赖实例化机制 | 抽象表达能力更强 |
可组合性 | 较差(需结构匹配) | 支持组合概念 |
✅ 在现代 C++ 中,CRTP 仍是 concepts 的低阶静态行为模拟手段。
Guideline 27:使用 CRTP 构建静态 Mixin 类(Static Mixins)
Mixin 是一种通过组合而非继承复用行为的技术。在 C++ 中,CRTP 非常适合做 静态 Mixin。
动机示例:添加强类型比较支持
template<typename Derived>
class EqualityComparable {
public:friend bool operator==(const Derived& lhs, const Derived& rhs) {return lhs.equals(rhs);}friend bool operator!=(const Derived& lhs, const Derived& rhs) {return !lhs.equals(rhs);}
};class Person : public EqualityComparable<Person> {std::string name;
public:bool equals(const Person& other) const {return name == other.name;}
};
用于静态能力增强的模式
boost::operators<>
std::enable_shared_from_this<T>
本质就是 CRTP Mixin- Policy-based Design 中每个 policy 类也是 CRTP mixin
总结
第六章系统揭示了三种 C++ 中非常典型的设计模式及其静态实现技巧:
模式名称 | 本质功能 | 推荐场景 |
---|---|---|
Adapter | 接口兼容/转换 | 第三方库/统一标准 |
Observer | 解耦事件通知机制 | GUI、MVC、数据流架构 |
CRTP | 静态多态与类型增强 | 零运行开销的行为复用、类型封装 |
它们分别强调了接口适配、事件解耦和静态行为增强的不同维度,是 C++ 软件设计中的“三剑客”。