装饰模式:动态扩展功能的艺术

说实话,装饰模式是我在实际项目中用得最多的设计模式之一。为什么?因为它解决了一个非常现实的问题——如何在不修改已有代码的前提下,给对象动态添加新功能

我记得刚入行那会儿,接手过一个老项目。代码里到处都是继承,一个类派生十几个子类,每个子类加一点功能。后来需求变了,要组合功能,结果子类数量爆炸式增长。嗯,那场面,简直是一场灾难。

模式动机:为什么需要装饰模式?

你想想看,如果我们要给一个对象添加功能,最直接的方式是什么?继承。但继承有个硬伤——它是静态的。编译时就把功能定死了,运行时没法改。

举个例子。假设你有一个Text类,用来显示文本。现在要加边框功能,你派生一个BorderedText。又要加滚动条,再派生一个ScrollableText。那既要边框又要滚动条呢?BorderedScrollableText。如果还要加阴影、加颜色、加背景……你算算要多少个类?

核心问题:继承导致类数量呈指数级增长,而且功能组合越复杂,代码越难维护。

装饰模式就是来解决这个问题的。它把功能拆成一个个独立的「装饰器」,像搭积木一样,运行时自由组合。说白了,就是用组合代替继承

模式定义

装饰模式(Decorator Pattern)——动态地给一个对象添加一些额外的职责。就扩展功能而言,它比生成子类更灵活。

我个人习惯把装饰模式理解为「俄罗斯套娃」。你一层套一层,每层加一点功能,最里面是核心对象,外面是各种装饰。

角色与结构

装饰模式有四个核心角色:

角色 名称 职责
Component 抽象组件 定义对象的接口,可以给这些对象动态添加职责
ConcreteComponent 具体组件 定义了一个具体的对象,可以给它添加职责
Decorator 抽象装饰类 持有Component引用,实现Component接口
ConcreteDecorator 具体装饰类 负责给Component添加具体功能

下面这张图,我画了装饰模式的核心结构。你看一眼就明白了。

<<interface>> Component ConcreteComponent Decorator - component: Component ConcreteDecoratorA ConcreteDecoratorB 装饰模式结构:一层套一层,功能自由组合

C++ 代码实现

光说不练假把式。咱们直接上代码。我写一个文本显示的装饰器例子。

// 抽象组件:文本显示接口
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();
    }
    
private:
    std::unique_ptr<Text> text_;
};

// 具体装饰:加边框
class BorderedText : public TextDecorator {
public:
    using TextDecorator::TextDecorator;
    
    std::string render() const override {
        return "┌─────────────┐\n"
               "│ " + TextDecorator::render() + " │\n"
               "└─────────────┘";
    }
};

// 具体装饰:加滚动条
class ScrollableText : public TextDecorator {
public:
    using TextDecorator::TextDecorator;
    
    std::string render() const override {
        return "▼\n" + TextDecorator::render() + "\n▲";
    }
};

// 使用示例
int main() {
    // 普通文本
    auto text = std::make_unique<PlainText>("Hello, World!");
    
    // 加边框
    auto bordered = std::make_unique<BorderedText>(std::move(text));
    
    // 再加滚动条
    auto scrollable = std::make_unique<ScrollableText>(std::move(bordered));
    
    std::cout << scrollable->render() << std::endl;
    
    return 0;
}

小提示:注意看,每个装饰器都持有被装饰对象的引用,并在自己的render()里调用被装饰对象的render(),再加上自己的功能。这就是「套娃」的核心。

动态扩展功能

装饰模式最大的优势是什么?运行时动态组合。你可以在程序运行时,根据条件决定给对象加什么功能。

我曾经在一个日志系统里用过装饰模式。核心日志器只负责写日志,然后根据需要动态添加时间戳、线程ID、日志级别等装饰器。配置一变,功能就变,完全不用改核心代码。

// 运行时动态组合
std::unique_ptr<Text> buildText(bool withBorder, bool withScroll) {
    auto text = std::make_unique<PlainText>("Hello, World!");
    
    if (withBorder) {
        text = std::make_unique<BorderedText>(std::move(text));
    }
    
    if (withScroll) {
        text = std::make_unique<ScrollableText>(std::move(text));
    }
    
    return text;
}

// 根据配置动态生成
auto text1 = buildText(true, false);   // 只有边框
auto text2 = buildText(false, true);   // 只有滚动条
auto text3 = buildText(true, true);    // 边框+滚动条

注意:装饰器链不要太长。我见过有人套了七八层,调试的时候根本分不清哪层出了什么问题。一般来说,3-4层以内比较合适。

避坑指南

我用装饰模式踩过几个坑,分享给你:

  • 装饰器顺序很重要——先加边框再加滚动条,和先加滚动条再加边框,结果可能完全不同。我建议在文档里明确标注装饰器的执行顺序。
  • 小心接口膨胀——如果Component接口太大,每个装饰器都要实现所有方法,那就麻烦了。保持接口小而精。
  • 不要滥用——如果功能组合是固定的,不会变化,那用继承可能更简单。装饰模式是为「动态变化」而生的。

我曾经在一个项目中,把装饰模式用在了不该用的地方。功能组合是固定的,结果代码绕来绕去,后来重构时全改成了继承。嗯,吃一堑长一智。

总结

装饰模式的核心思想就一句话:用组合代替继承,运行时动态扩展功能

它特别适合以下场景:

  • 需要给对象动态添加功能,且功能可以自由组合
  • 不想通过继承产生大量子类
  • 功能需要运行时决定,而不是编译时

说白了,装饰模式就是让你像搭积木一样搭功能。每个装饰器只管自己的事,组合起来却千变万化。这就是它的魅力所在。


公众号:蓝海资料掘金营,微信deep3321