
C++装饰器模式实现方法:让功能扩展像“套娃”一样优雅
大家好,今天我想和大家深入聊聊C++设计模式中一个既实用又优雅的模式——装饰器模式(Decorator Pattern)。我第一次真正理解它的妙处,是在为一个老旧的日志系统添加新功能时。当时,系统已经有了基础的日志输出,但产品经理突然要求:既要能输出到文件,又要能同时输出到网络,还要能根据级别过滤,甚至给日志加上时间戳和线程ID。如果直接在原有类上“打补丁”,代码会迅速变成一团乱麻。而装饰器模式,就像给一个核心对象套上多层“包装纸”,每层纸增加一点新功能,最终完美解决了这个难题。下面,我就结合自己的实战经验,带大家一步步实现它。
一、什么是装饰器模式?先理解“套娃”思想
装饰器模式的核心思想是动态地给一个对象添加一些额外的职责。它属于结构型模式,是继承的一种灵活替代方案。你可以把它想象成俄罗斯套娃:最里面是核心对象(基础功能),外面的每一层套娃都是一个“装饰器”,为它增加新的外观或能力(如彩绘、镶金边)。
为什么不用继承? 继承是静态的,如果我要“文件日志+网络日志+时间戳”这个组合,就得创建一个新子类。如果组合方式有N种,就需要创建N个子类,这就是可怕的“类爆炸”。装饰器模式通过组合而非继承,在运行时灵活地组装功能,完美避开了这个坑。
它的主要角色有四个:
- 组件接口(Component):定义核心对象和装饰器的共同接口。
- 具体组件(ConcreteComponent):核心对象,实现基础功能。
- 装饰器基类(Decorator):继承组件接口,并持有一个组件对象的引用。
- 具体装饰器(ConcreteDecorator):负责给组件添加新的职责。
二、实战场景:构建一个可扩展的日志系统
让我们用上面提到的日志系统作为例子。假设我们最初只有一个简单的日志器,只能输出纯文本消息。
首先,定义我们的“组件接口” ILogger:
// 组件接口:所有日志器的抽象
class ILogger {
public:
virtual ~ILogger() = default;
virtual void log(const std::string& message) = 0;
};
接着,实现最核心的“具体组件” SimpleLogger,它只负责最基本的输出到控制台:
// 具体组件:基础日志器
class SimpleLogger : public ILogger {
public:
void log(const std::string& message) override {
std::cout << "Log: " << message << std::endl;
}
};
好了,基础打好了。现在,关键角色“装饰器基类”要登场了。这是实现模式的核心技巧:
// 装饰器基类:继承接口并包含一个组件引用
class LoggerDecorator : public ILogger {
protected:
std::unique_ptr wrappedLogger; // 持有一个被装饰的日志器
public:
// 通过构造函数注入需要被装饰的对象
explicit LoggerDecorator(std::unique_ptr logger)
: wrappedLogger(std::move(logger)) {}
// 默认实现:直接转发给被装饰的对象
void log(const std::string& message) override {
if (wrappedLogger) {
wrappedLogger->log(message);
}
}
};
请注意,LoggerDecorator 本身也实现了 log 方法,但它只是简单转发。它的存在是为了让具体的装饰器继承,并在此基础上“增强”功能。
三、实现具体装饰器:给日志穿上“功能外衣”
现在,我们可以开始创造各种装饰器了。每个装饰器继承自 LoggerDecorator,在调用被装饰对象的 log 方法前后,添加自己的逻辑。
1. 时间戳装饰器:给每条日志加上当前时间。
#include
#include
#include
class TimestampLogger : public LoggerDecorator {
public:
using LoggerDecorator::LoggerDecorator; // 继承构造函数
void log(const std::string& message) override {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss <log(ss.str() + message);
}
};
2. 错误级别装饰器:只记录错误级别以上的日志。
enum class LogLevel { DEBUG, INFO, WARNING, ERROR };
class LevelFilterLogger : public LoggerDecorator {
LogLevel minLevel;
public:
LevelFilterLogger(std::unique_ptr logger, LogLevel level)
: LoggerDecorator(std::move(logger)), minLevel(level) {}
void log(const std::string& message) override {
// 假设message格式为 "[LEVEL] msg",这里简单演示
// 实际中可能需要解析message的第一个词
// 这里为了演示,我们假设所有消息都通过
wrappedLogger->log(message);
}
// 更完整的实现应该有一个带Level参数的log函数,这里做了简化
};
3. 文件输出装饰器:将日志写入文件。
#include
class FileLogger : public LoggerDecorator {
std::ofstream fileStream;
public:
FileLogger(std::unique_ptr logger, const std::string& filename)
: LoggerDecorator(std::move(logger)), fileStream(filename) {
if (!fileStream.is_open()) {
throw std::runtime_error("Failed to open log file!");
}
}
void log(const std::string& message) override {
// 1. 先调用被装饰的logger(比如带时间戳的)
wrappedLogger->log(message);
// 2. 再额外写入文件
fileStream << message << std::endl;
}
};
四、组合使用:体验动态装配的强大
最激动人心的部分来了!现在我们可以在客户端代码中,像搭积木一样组合这些功能:
int main() {
// 1. 基础日志器
auto logger = std::make_unique();
// 2. 动态添加时间戳功能:套上第一层“装饰”
logger = std::make_unique(std::move(logger));
// 3. 动态添加文件输出功能:再套一层
// 现在日志会同时输出到控制台(带时间戳)和文件
logger = std::make_unique(std::move(logger), "app.log");
// 使用最终组装好的logger
logger->log("System started successfully.");
logger->log("User 'Alice' logged in.");
logger->log("ERROR: Database connection failed!");
return 0;
}
运行这段代码,你会在控制台看到带时间戳的日志,同时所有内容也会被写入 app.log 文件。如果想改变组合,比如只想输出带时间戳的日志到控制台而不写文件,只需不添加 FileLogger 那层装饰即可。完全无需修改任何现有类的代码!
五、实战踩坑与最佳实践
在项目中使用装饰器模式,我总结了几点经验和注意事项:
1. 智能指针管理所有权:上面的例子使用了 std::unique_ptr 来明确对象所有权转移。这是现代C++的推荐做法,能有效防止内存泄漏。装饰器“包裹”了原始对象,并接管了它的生命周期。
2. 注意装饰顺序:装饰的顺序可能影响结果。比如,先加时间戳再过滤级别,和先过滤级别再加时间戳,逻辑是不同的。需要根据业务需求确定装饰器的调用链顺序。
3. 避免过度装饰:虽然灵活,但嵌套层数过多会影响性能和调试难度。如果装饰链非常长,可能需要考虑其他模式(如责任链模式)或重构。
4. 接口设计的稳定性:组件接口(如 ILogger)一旦确定,应尽量保持稳定。因为所有装饰器都依赖它,频繁修改接口会导致大量改动。
5. 与代理模式的区别:新手容易混淆装饰器和代理模式。简单来说,装饰器目的是增强功能,而代理目的是控制访问(如延迟加载、权限检查)。虽然结构相似,但意图不同。
六、总结
装饰器模式通过一种松耦合的方式,实现了功能的动态扩展,完美遵循了“开放-封闭原则”(对扩展开放,对修改封闭)。在C++中,利用继承和组合,配合智能指针,可以优雅地实现它。下次当你面临“如何在不修改原有类的情况下增加功能”这个经典难题时,不妨想想这个“套娃”模型,它很可能就是那把优雅的钥匙。
希望这篇结合实战的教程能帮助你掌握C++装饰器模式的精髓。如果在实现中遇到问题,欢迎讨论。编程的艺术,往往就在于如何将复杂的需求,分解成一个个可以灵活组合的简单模块。Happy coding!

评论(0)