11. 装饰器模式(Decorator):动态扩展功能,与继承的比较,C++标准库中的应用

先聊聊我为什么喜欢装饰器模式

做C++开发这些年,我遇到过不少“类爆炸”的场景。比如一个文本处理系统,基础功能是读取文本,然后要支持加粗、斜体、下划线、颜色、字体大小……你想想看,如果用继承去实现,得有多少组合?

我记得有个项目,一开始只要求加粗和斜体。产品经理说“就两个功能,简单”。结果上线三个月,需求变成了“加粗+斜体”、“加粗+下划线”、“斜体+下划线”、“加粗+斜体+下划线”……我当时的表情,嗯,你懂的。

装饰器模式就是专门解决这个问题的。它不搞继承那一套,而是用“包装”的方式,一层层给对象加功能。说白了,就像你穿衣服——先穿衬衫,再套外套,再围围巾。每件衣服都是独立的,想穿几件穿几件,想脱哪件脱哪件。

装饰器模式的核心思想

装饰器模式的定义其实很简单:动态地给一个对象添加额外的职责。就这一句话,但背后的设计哲学很深。

它的结构是这样的:

  • Component(抽象组件):定义对象的接口
  • ConcreteComponent(具体组件):被装饰的原始对象
  • Decorator(抽象装饰器):持有Component引用,实现相同接口
  • ConcreteDecorator(具体装饰器):真正添加功能的类

我画了一张图,帮你理清它们的关系:

Component +operation() ConcreteComponent +operation() Decorator -component: Component* ConcreteDecoratorA +operation() ConcreteDecoratorB +operation() 装饰器模式结构图:一层层包装,动态扩展功能

代码示例:一个文本装饰器

光说不练假把式。我写个实际例子,你一看就明白。

// 抽象组件:文本接口
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,从最外层装饰器一直追到最里层,花了整整一下午
  • 性能敏感场景:每层装饰器都有虚函数调用开销。如果每秒调用百万次,这个开销不可忽视

说白了,装饰器模式是个好工具,但不是万能钥匙。用对了地方,代码优雅得像首诗;用错了地方,调试起来像噩梦。

我个人习惯是:先问自己三个问题——功能会不会变?组合多不多?性能要求高不高?想清楚了再决定用不用装饰器。


专注资料整理