装饰器模式:给代码穿上“日志外套”

说实话,装饰器模式是我在实际项目中用得最频繁的结构型模式之一。它解决了一个很实际的问题:如何在不修改原有类的情况下,给对象动态添加新功能

我记得有一次接手一个老项目,里面有个数据处理器,核心逻辑跑得好好的。但产品经理突然说:“每个操作都要加日志,方便排查问题。” 你想想看,如果直接改那个类,不仅破坏了开闭原则,还可能引入新bug。这时候装饰器模式就派上用场了。

模式本质:一层套一层

装饰器模式的核心思想其实很简单:用组合代替继承。它通过创建一个包装类(装饰器),在调用原始对象方法的前后,插入额外的逻辑。

说白了,就像你给手机套个壳——手机本身的功能没变,但多了防摔、美观这些“装饰”。

关键角色:

  • Component(抽象组件):定义接口,声明原始操作
  • ConcreteComponent(具体组件):实现接口,提供基础功能
  • Decorator(抽象装饰器):持有Component引用,实现相同接口
  • ConcreteDecorator(具体装饰器):在调用前后添加额外逻辑
Component + execute() ConcreteComponent + execute() Decorator - component: Component LogDecorator + execute() 实现 实现 持有 继承 图例 实现关系 聚合关系(持有引用) 继承关系

实战:给数据处理器加日志

咱们直接看代码。假设有一个数据处理器接口,负责处理数据:

// 抽象组件
class DataProcessor {
public:
    virtual ~DataProcessor() = default;
    virtual void process(const std::string& data) = 0;
};

// 具体组件:基础处理器
class BasicProcessor : public DataProcessor {
public:
    void process(const std::string& data) override {
        std::cout << "处理数据: " << data << std::endl;
        // 实际处理逻辑...
    }
};

现在,我想给这个处理器加日志功能。按照装饰器模式,先定义一个抽象装饰器:

// 抽象装饰器
class ProcessorDecorator : public DataProcessor {
protected:
    std::unique_ptr<DataProcessor> component_;
public:
    ProcessorDecorator(std::unique_ptr<DataProcessor> comp) 
        : component_(std::move(comp)) {}
    
    void process(const std::string& data) override {
        if (component_) {
            component_->process(data);
        }
    }
};

然后实现具体的日志装饰器:

// 具体装饰器:日志装饰器
class LoggingDecorator : public ProcessorDecorator {
public:
    using ProcessorDecorator::ProcessorDecorator;
    
    void process(const std::string& data) override {
        std::cout << "[日志] 开始处理数据..." << std::endl;
        auto start = std::chrono::steady_clock::now();
        
        // 调用被装饰对象的方法
        ProcessorDecorator::process(data);
        
        auto end = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
        std::cout << "[日志] 处理完成,耗时: " << duration << "ms" << std::endl;
    }
};

使用技巧: 装饰器可以多层嵌套。比如先加日志,再加权限校验,再加缓存——每层只关注自己的职责。我习惯用工厂方法创建装饰链,这样客户端代码很干净。

客户端怎么用?

int main() {
    // 创建基础处理器
    auto processor = std::make_unique<BasicProcessor>();
    
    // 用日志装饰器包装
    auto loggedProcessor = std::make_unique<LoggingDecorator>(std::move(processor));
    
    // 调用时自动记录日志
    loggedProcessor->process("用户订单数据");
    
    return 0;
}

输出结果:

[日志] 开始处理数据...
处理数据: 用户订单数据
[日志] 处理完成,耗时: 0ms

避坑指南

我曾经踩过的一个坑: 装饰器模式里,如果装饰器和被装饰对象共享同一个接口,但装饰器内部不小心调用了自己的方法而不是委托给组件,就会造成无限递归。嗯,调试起来特别头疼。

另外,有几点需要注意:

  • 接口要稳定:装饰器依赖接口,如果接口频繁变动,所有装饰器都得改
  • 不要过度装饰:嵌套太多层,调试时堆栈会变得很深,影响性能
  • 考虑线程安全:如果装饰器在多线程环境下使用,日志输出可能需要加锁

什么时候用装饰器?

场景 推荐程度 说明
给现有类加日志、监控、统计 ⭐⭐⭐⭐⭐ 完美契合,不改原有代码
需要动态组合多种功能 ⭐⭐⭐⭐ 比继承灵活,但别嵌套太多
功能组合是固定的 ⭐⭐⭐ 如果组合不变,直接用继承更简单
需要大量修改接口 接口不稳定时,装饰器维护成本高

我的个人习惯: 在项目初期,我会先写一个基础版本,不加任何装饰。等需求明确后,再用装饰器逐步添加横切关注点(日志、鉴权、缓存)。这样既保证了代码的灵活性,又不会过度设计。

装饰器模式的核心价值在于:它让“添加功能”这件事变得可组合、可复用、可插拔。你想想看,如果没有装饰器,我们可能得在每个方法里手动加日志,或者用AOP(面向切面编程)——但C++里AOP并不原生支持。装饰器模式,恰恰是C++世界里实现类似效果最优雅的方式。

专注资料整理