11. 装饰器模式(Decorator):动态扩展功能,与继承的比较,C++标准库中的应用
先聊聊我为什么喜欢装饰器模式
做C++开发这些年,我遇到过不少“类爆炸”的场景。比如一个文本处理系统,基础功能是读取文本,然后要支持加粗、斜体、下划线、颜色、字体大小……你想想看,如果用继承去实现,得有多少组合?
我记得有个项目,一开始只要求加粗和斜体。产品经理说“就两个功能,简单”。结果上线三个月,需求变成了“加粗+斜体”、“加粗+下划线”、“斜体+下划线”、“加粗+斜体+下划线”……我当时的表情,嗯,你懂的。
装饰器模式就是专门解决这个问题的。它不搞继承那一套,而是用“包装”的方式,一层层给对象加功能。说白了,就像你穿衣服——先穿衬衫,再套外套,再围围巾。每件衣服都是独立的,想穿几件穿几件,想脱哪件脱哪件。
装饰器模式的核心思想
装饰器模式的定义其实很简单:动态地给一个对象添加额外的职责。就这一句话,但背后的设计哲学很深。
它的结构是这样的:
- Component(抽象组件):定义对象的接口
- ConcreteComponent(具体组件):被装饰的原始对象
- Decorator(抽象装饰器):持有Component引用,实现相同接口
- ConcreteDecorator(具体装饰器):真正添加功能的类
我画了一张图,帮你理清它们的关系:
代码示例:一个文本装饰器
光说不练假把式。我写个实际例子,你一看就明白。
// 抽象组件:文本接口
class Text {
public:
virtual ~Text() = default;
virtual std::string render() const = 0;
};
// 具体组件:普通文本
class PlainText : public Text {
public:
explicit PlainText(std::string content) : content_(std::move(content)) {}
std::string render() const override {
return content_;
}
private:
std::string content_;
};
// 抽象装饰器
class TextDecorator : public Text {
public:
explicit TextDecorator(std::unique_ptr<Text> text)
: text_(std::move(text)) {}
std::string render() const override {
return text_->render();
}
protected:
std::unique_ptr<Text> text_;
};
// 具体装饰器:加粗
class BoldDecorator : public TextDecorator {
public:
using TextDecorator::TextDecorator;
std::string render() const override {
return "<b>" + text_->render() + "</b>";
}
};
// 具体装饰器:斜体
class ItalicDecorator : public TextDecorator {
public:
using TextDecorator::TextDecorator;
std::string render() const override {
return "<i>" + text_->render() + "</i>";
}
};
// 使用示例
int main() {
auto text = std::make_unique<PlainText>("Hello, World!");
// 加粗
auto bold = std::make_unique<BoldDecorator>(std::move(text));
// 加粗 + 斜体
auto boldItalic = std::make_unique<ItalicDecorator>(std::move(bold));
std::cout << boldItalic->render() << std::endl;
// 输出: <i><b>Hello, World!</b></i>
return 0;
}
关键点:装饰器模式的核心是“组合优于继承”。每个装饰器只关心自己那层逻辑,不关心其他层。想加功能就再包一层,想减功能就少包一层。
装饰器 vs 继承:我的实战对比
我在项目中两种方式都用过,踩过坑也尝过甜头。直接给你对比表:
| 维度 | 继承 | 装饰器模式 |
|---|---|---|
| 扩展方式 | 编译期静态扩展 | 运行期动态扩展 |
| 类数量 | 组合爆炸(2^n) | 线性增长(n) |
| 灵活性 | 低,继承链固定 | 高,可任意组合 |
| 透明性 | 子类可覆盖父类行为 | 对客户端透明 |
| 适用场景 | 功能稳定,层次明确 | 功能多变,需要动态组合 |
我的建议:如果功能组合少于5种,用继承更简单。如果超过5种,或者需求经常变,果断上装饰器。我曾经在一个日志系统里用了装饰器,后来加加密、加压缩、加格式化,一行现有代码都没改。
C++标准库中的装饰器应用
你可能没注意到,C++标准库其实早就用了装饰器思想。我举几个例子:
- std::unique_ptr 的自定义删除器:你可以在智能指针上“装饰”一个删除器,改变它的销毁行为
- std::ostream 的流操作符:std::cout << std::hex << value,hex就是个装饰器,改变了输出格式
- std::function 的包装:你可以用lambda包装一个函数,添加日志、计时等功能
我记得有一次做性能分析,需要统计每个函数的执行时间。我写了个简单的装饰器:
// 计时装饰器
template<typename Func>
auto timed(Func func) {
return [func](auto&&... args) {
auto start = std::chrono::high_resolution_clock::now();
auto result = func(std::forward<decltype(args)>(args)...);
auto end = std::chrono::high_resolution_clock::now();
std::cout << "耗时: "
<< std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()
<< "us" << std::endl;
return result;
};
}
// 使用
auto slowFunc = []() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return 42;
};
auto timedFunc = timed(slowFunc);
timedFunc(); // 输出: 耗时: 100xxx us
避坑指南:我曾经在装饰器里忘了转发参数,导致完美转发失效。记住,装饰器必须保持接口完全一致,包括const、引用、异常说明。否则客户端代码会莫名其妙地编译失败。
什么时候不该用装饰器?
任何模式都有适用边界。我总结了几种情况:
- 对象类型需要被识别:如果客户端需要知道“这个对象到底是加粗还是斜体”,装饰器就不合适。因为装饰器对客户端是透明的
- 装饰器链太深:超过5层装饰,调试起来很痛苦。我曾经追过一个bug,从最外层装饰器一直追到最里层,花了整整一下午
- 性能敏感场景:每层装饰器都有虚函数调用开销。如果每秒调用百万次,这个开销不可忽视
说白了,装饰器模式是个好工具,但不是万能钥匙。用对了地方,代码优雅得像首诗;用错了地方,调试起来像噩梦。
我个人习惯是:先问自己三个问题——功能会不会变?组合多不多?性能要求高不高?想清楚了再决定用不用装饰器。