装饰器模式:给代码穿上“日志外套”
说实话,装饰器模式是我在实际项目中用得最频繁的结构型模式之一。它解决了一个很实际的问题:如何在不修改原有类的情况下,给对象动态添加新功能。
我记得有一次接手一个老项目,里面有个数据处理器,核心逻辑跑得好好的。但产品经理突然说:“每个操作都要加日志,方便排查问题。” 你想想看,如果直接改那个类,不仅破坏了开闭原则,还可能引入新bug。这时候装饰器模式就派上用场了。
模式本质:一层套一层
装饰器模式的核心思想其实很简单:用组合代替继承。它通过创建一个包装类(装饰器),在调用原始对象方法的前后,插入额外的逻辑。
说白了,就像你给手机套个壳——手机本身的功能没变,但多了防摔、美观这些“装饰”。
关键角色:
- Component(抽象组件):定义接口,声明原始操作
- ConcreteComponent(具体组件):实现接口,提供基础功能
- Decorator(抽象装饰器):持有Component引用,实现相同接口
- ConcreteDecorator(具体装饰器):在调用前后添加额外逻辑
实战:给数据处理器加日志
咱们直接看代码。假设有一个数据处理器接口,负责处理数据:
// 抽象组件
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++世界里实现类似效果最优雅的方式。