装饰模式:让功能像乐高一样自由组合
说实话,我第一次接触装饰模式时,脑子里冒出的画面就是俄罗斯套娃。一层套一层,每层加点新东西。后来在项目中真正用起来,才发现这玩意儿比套娃灵活多了——它更像乐高积木,你想给对象加什么功能,就往上拼一块,随时拼,随时拆。
我记得有一次接手一个老项目,里面有个日志系统。需求很简单:记录用户操作。但后来产品经理不断加需求——要加密、要压缩、要异步发送、要加时间戳……每次改需求,我都得去改那个核心类。改到最后,那个类膨胀得像个气球,谁都不敢碰。那时候我就想:要是能像穿衣服一样,今天穿外套,明天戴围巾,该多好?
嗯,装饰模式就是干这个的。
模式动机与定义
先说说动机。你想想看,软件开发中最头疼的是什么?是变化。今天要加A功能,明天要改B逻辑。如果用继承来扩展功能,你会发现类数量爆炸——每个组合都要一个新类。比如咖啡店:美式、拿铁、摩卡、卡布奇诺……再加糖、加奶、加奶油……排列组合下来,类多得能绕地球三圈。
装饰模式的核心思想很简单:不修改原有类,也不通过继承来扩展,而是把功能类像装饰品一样,动态地"包裹"到核心对象上。
定义:装饰模式(Decorator Pattern)允许向一个现有的对象动态添加新的功能,同时又不改变其结构。它属于结构型设计模式。
说白了,就是给对象"穿衣服"。核心对象是"人",你想让他穿衬衫,就包一层衬衫;想让他穿外套,再包一层外套。每层互不干扰,而且可以自由组合。
角色与结构
装饰模式有四个角色,我习惯这么记:
- Component(抽象构件):定义核心接口。就是那个"人"的抽象。
- ConcreteComponent(具体构件):核心对象。比如一个具体的"人"。
- Decorator(抽象装饰类):继承自Component,同时持有Component的引用。这是关键——它既"是"那个东西,又"有"那个东西。
- ConcreteDecorator(具体装饰类):具体的装饰功能。比如"穿衬衫"、"穿外套"。
结构图长这样:
看到那个虚线箭头了吗?那是装饰模式的核心——装饰类持有被装饰对象的引用。这样它才能调用被装饰对象的方法,并在前后加上自己的逻辑。
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, "<");
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:<script>alert('xss')</script>]
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层以内,超过这个数,考虑用其他模式或者重构。
另外,装饰模式还有一个容易踩的坑——装饰顺序影响结果。比如先加密再压缩,和先压缩再加密,结果完全不同。所以设计时要明确装饰的顺序语义,最好在文档里写清楚。
最后说一句:装饰模式和继承不是二选一的关系。我个人的经验是,能用组合解决的问题,尽量不用继承。装饰模式就是组合思想的一个经典体现。它让代码更灵活,更可测试,也更符合开闭原则——对扩展开放,对修改关闭。
嗯,装饰模式就聊到这儿。记住它的核心:动态地给对象添加职责,比生成子类更灵活。下次遇到功能不断膨胀的类,不妨想想——能不能用装饰模式给它"瘦瘦身"?