装饰模式:动态扩展功能的艺术
说实话,装饰模式是我在实际项目中用得最多的设计模式之一。为什么?因为它解决了一个非常现实的问题——如何在不修改已有代码的前提下,给对象动态添加新功能。
我记得刚入行那会儿,接手过一个老项目。代码里到处都是继承,一个类派生十几个子类,每个子类加一点功能。后来需求变了,要组合功能,结果子类数量爆炸式增长。嗯,那场面,简直是一场灾难。
模式动机:为什么需要装饰模式?
你想想看,如果我们要给一个对象添加功能,最直接的方式是什么?继承。但继承有个硬伤——它是静态的。编译时就把功能定死了,运行时没法改。
举个例子。假设你有一个Text类,用来显示文本。现在要加边框功能,你派生一个BorderedText。又要加滚动条,再派生一个ScrollableText。那既要边框又要滚动条呢?BorderedScrollableText。如果还要加阴影、加颜色、加背景……你算算要多少个类?
核心问题:继承导致类数量呈指数级增长,而且功能组合越复杂,代码越难维护。
装饰模式就是来解决这个问题的。它把功能拆成一个个独立的「装饰器」,像搭积木一样,运行时自由组合。说白了,就是用组合代替继承。
模式定义
装饰模式(Decorator Pattern)——动态地给一个对象添加一些额外的职责。就扩展功能而言,它比生成子类更灵活。
我个人习惯把装饰模式理解为「俄罗斯套娃」。你一层套一层,每层加一点功能,最里面是核心对象,外面是各种装饰。
角色与结构
装饰模式有四个核心角色:
| 角色 | 名称 | 职责 |
|---|---|---|
| Component | 抽象组件 | 定义对象的接口,可以给这些对象动态添加职责 |
| ConcreteComponent | 具体组件 | 定义了一个具体的对象,可以给它添加职责 |
| Decorator | 抽象装饰类 | 持有Component引用,实现Component接口 |
| ConcreteDecorator | 具体装饰类 | 负责给Component添加具体功能 |
下面这张图,我画了装饰模式的核心结构。你看一眼就明白了。
C++ 代码实现
光说不练假把式。咱们直接上代码。我写一个文本显示的装饰器例子。
// 抽象组件:文本显示接口
class Text {
public:
virtual ~Text() = default;
virtual std::string render() const = 0;
};
// 具体组件:普通文本
class PlainText : public Text {
public:
explicit PlainText(std::string content) : content_(std::move(content)) {}
std::string render() const override {
return content_;
}
private:
std::string content_;
};
// 抽象装饰类
class TextDecorator : public Text {
public:
explicit TextDecorator(std::unique_ptr<Text> text)
: text_(std::move(text)) {}
std::string render() const override {
return text_->render();
}
private:
std::unique_ptr<Text> text_;
};
// 具体装饰:加边框
class BorderedText : public TextDecorator {
public:
using TextDecorator::TextDecorator;
std::string render() const override {
return "┌─────────────┐\n"
"│ " + TextDecorator::render() + " │\n"
"└─────────────┘";
}
};
// 具体装饰:加滚动条
class ScrollableText : public TextDecorator {
public:
using TextDecorator::TextDecorator;
std::string render() const override {
return "▼\n" + TextDecorator::render() + "\n▲";
}
};
// 使用示例
int main() {
// 普通文本
auto text = std::make_unique<PlainText>("Hello, World!");
// 加边框
auto bordered = std::make_unique<BorderedText>(std::move(text));
// 再加滚动条
auto scrollable = std::make_unique<ScrollableText>(std::move(bordered));
std::cout << scrollable->render() << std::endl;
return 0;
}
小提示:注意看,每个装饰器都持有被装饰对象的引用,并在自己的render()里调用被装饰对象的render(),再加上自己的功能。这就是「套娃」的核心。
动态扩展功能
装饰模式最大的优势是什么?运行时动态组合。你可以在程序运行时,根据条件决定给对象加什么功能。
我曾经在一个日志系统里用过装饰模式。核心日志器只负责写日志,然后根据需要动态添加时间戳、线程ID、日志级别等装饰器。配置一变,功能就变,完全不用改核心代码。
// 运行时动态组合
std::unique_ptr<Text> buildText(bool withBorder, bool withScroll) {
auto text = std::make_unique<PlainText>("Hello, World!");
if (withBorder) {
text = std::make_unique<BorderedText>(std::move(text));
}
if (withScroll) {
text = std::make_unique<ScrollableText>(std::move(text));
}
return text;
}
// 根据配置动态生成
auto text1 = buildText(true, false); // 只有边框
auto text2 = buildText(false, true); // 只有滚动条
auto text3 = buildText(true, true); // 边框+滚动条
注意:装饰器链不要太长。我见过有人套了七八层,调试的时候根本分不清哪层出了什么问题。一般来说,3-4层以内比较合适。
避坑指南
我用装饰模式踩过几个坑,分享给你:
- 装饰器顺序很重要——先加边框再加滚动条,和先加滚动条再加边框,结果可能完全不同。我建议在文档里明确标注装饰器的执行顺序。
- 小心接口膨胀——如果Component接口太大,每个装饰器都要实现所有方法,那就麻烦了。保持接口小而精。
- 不要滥用——如果功能组合是固定的,不会变化,那用继承可能更简单。装饰模式是为「动态变化」而生的。
我曾经在一个项目中,把装饰模式用在了不该用的地方。功能组合是固定的,结果代码绕来绕去,后来重构时全改成了继承。嗯,吃一堑长一智。
总结
装饰模式的核心思想就一句话:用组合代替继承,运行时动态扩展功能。
它特别适合以下场景:
- 需要给对象动态添加功能,且功能可以自由组合
- 不想通过继承产生大量子类
- 功能需要运行时决定,而不是编译时
说白了,装饰模式就是让你像搭积木一样搭功能。每个装饰器只管自己的事,组合起来却千变万化。这就是它的魅力所在。
公众号:蓝海资料掘金营,微信deep3321