装饰模式:让功能像乐高一样自由组合

说实话,我第一次接触装饰模式时,脑子里冒出的画面就是俄罗斯套娃。一层套一层,每层加点新东西。后来在项目中真正用起来,才发现这玩意儿比套娃灵活多了——它更像乐高积木,你想给对象加什么功能,就往上拼一块,随时拼,随时拆。

我记得有一次接手一个老项目,里面有个日志系统。需求很简单:记录用户操作。但后来产品经理不断加需求——要加密、要压缩、要异步发送、要加时间戳……每次改需求,我都得去改那个核心类。改到最后,那个类膨胀得像个气球,谁都不敢碰。那时候我就想:要是能像穿衣服一样,今天穿外套,明天戴围巾,该多好?

嗯,装饰模式就是干这个的。

模式动机与定义

先说说动机。你想想看,软件开发中最头疼的是什么?是变化。今天要加A功能,明天要改B逻辑。如果用继承来扩展功能,你会发现类数量爆炸——每个组合都要一个新类。比如咖啡店:美式、拿铁、摩卡、卡布奇诺……再加糖、加奶、加奶油……排列组合下来,类多得能绕地球三圈。

装饰模式的核心思想很简单:不修改原有类,也不通过继承来扩展,而是把功能类像装饰品一样,动态地"包裹"到核心对象上

定义:装饰模式(Decorator Pattern)允许向一个现有的对象动态添加新的功能,同时又不改变其结构。它属于结构型设计模式。

说白了,就是给对象"穿衣服"。核心对象是"人",你想让他穿衬衫,就包一层衬衫;想让他穿外套,再包一层外套。每层互不干扰,而且可以自由组合。

角色与结构

装饰模式有四个角色,我习惯这么记:

  • Component(抽象构件):定义核心接口。就是那个"人"的抽象。
  • ConcreteComponent(具体构件):核心对象。比如一个具体的"人"。
  • Decorator(抽象装饰类):继承自Component,同时持有Component的引用。这是关键——它既"是"那个东西,又"有"那个东西。
  • ConcreteDecorator(具体装饰类):具体的装饰功能。比如"穿衬衫"、"穿外套"。

结构图长这样:

Component +operation() ConcreteComponent +operation() Decorator -component: Component +operation() ConcreteDecoratorA +operation() ConcreteDecoratorB +operation() 继承 继承 继承 继承 持有引用

看到那个虚线箭头了吗?那是装饰模式的核心——装饰类持有被装饰对象的引用。这样它才能调用被装饰对象的方法,并在前后加上自己的逻辑。

C++代码实现

光说不练假把式。我们写个实际的例子:一个文本处理系统。核心功能是输出文本,然后我们可以动态地给它加加密、加压缩、加HTML转义。

先定义抽象构件:

// 抽象构件:文本处理器
class TextProcessor {
public:
    virtual ~TextProcessor() = default;
    virtual std::string process(const std::string& input) = 0;
};

具体构件:

// 具体构件:基础文本处理器
class PlainTextProcessor : public TextProcessor {
public:
    std::string process(const std::string& input) override {
        return input;
    }
};

抽象装饰类:

// 抽象装饰类
class TextDecorator : public TextProcessor {
protected:
    std::unique_ptr<TextProcessor> component_;
public:
    TextDecorator(std::unique_ptr<TextProcessor> comp) 
        : component_(std::move(comp)) {}
    
    std::string process(const std::string& input) override {
        return component_->process(input);
    }
};

具体装饰类:

// 具体装饰:HTML转义
class HtmlEscapeDecorator : public TextDecorator {
public:
    using TextDecorator::TextDecorator;
    
    std::string process(const std::string& input) override {
        std::string escaped = component_->process(input);
        // 简单的HTML转义
        size_t pos = 0;
        while ((pos = escaped.find('<', pos)) != std::string::npos) {
            escaped.replace(pos, 1, "&lt;");
            pos += 4;
        }
        return escaped;
    }
};

// 具体装饰:Base64加密(示意)
class Base64Decorator : public TextDecorator {
public:
    using TextDecorator::TextDecorator;
    
    std::string process(const std::string& input) override {
        std::string data = component_->process(input);
        // 这里只是示意,实际Base64编码要复杂得多
        return "[BASE64:" + data + "]";
    }
};

使用方式:

int main() {
    // 基础处理器
    auto processor = std::make_unique<PlainTextProcessor>();
    
    // 先转义,再加密
    auto decorated = std::make_unique<Base64Decorator>(
        std::make_unique<HtmlEscapeDecorator>(std::move(processor))
    );
    
    std::string result = decorated->process("<script>alert('xss')</script>");
    // 输出: [BASE64:&lt;script&gt;alert('xss')&lt;/script&gt;]
    
    return 0;
}

小提示:我在项目中习惯用 std::unique_ptr 来管理装饰链的生命周期。这样内存管理清晰,而且移动语义让代码更高效。如果你用原始指针,记得要处理好所有权问题,否则容易内存泄漏。

动态扩展功能

装饰模式最爽的地方在于——运行时动态组合。你可以在程序运行时,根据配置或用户输入,决定给对象穿几层"衣服"。

举个例子:

std::unique_ptr<TextProcessor> buildProcessor(const std::vector<std::string>& features) {
    std::unique_ptr<TextProcessor> proc = std::make_unique<PlainTextProcessor>();
    
    for (const auto& feat : features) {
        if (feat == "html_escape") {
            proc = std::make_unique<HtmlEscapeDecorator>(std::move(proc));
        } else if (feat == "base64") {
            proc = std::make_unique<Base64Decorator>(std::move(proc));
        }
        // 可以继续加其他装饰
    }
    return proc;
}

// 使用
auto proc = buildProcessor({"html_escape", "base64"});
std::cout << proc->process("<hello>");

你看,功能组合完全由调用方决定。今天要加密+压缩,明天要压缩+转义,改一下配置就行,核心代码一行都不用动。

注意:装饰模式虽然灵活,但也不是万能的。我曾经在一个项目里过度使用装饰模式,结果装饰链嵌套了七八层,调试时根本看不清数据流。后来我加了个日志装饰器,专门打印每层的输入输出,才把问题定位到。所以我的建议是:装饰层数控制在3-5层以内,超过这个数,考虑用其他模式或者重构。

另外,装饰模式还有一个容易踩的坑——装饰顺序影响结果。比如先加密再压缩,和先压缩再加密,结果完全不同。所以设计时要明确装饰的顺序语义,最好在文档里写清楚。

最后说一句:装饰模式和继承不是二选一的关系。我个人的经验是,能用组合解决的问题,尽量不用继承。装饰模式就是组合思想的一个经典体现。它让代码更灵活,更可测试,也更符合开闭原则——对扩展开放,对修改关闭。

嗯,装饰模式就聊到这儿。记住它的核心:动态地给对象添加职责,比生成子类更灵活。下次遇到功能不断膨胀的类,不妨想想——能不能用装饰模式给它"瘦瘦身"?


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