← 返回题目列表

装饰器模式相比继承有什么优势?

高频 中等 第 9 / 25 题 更新于 2026/07/28
装饰器模式继承组合复用开闭原则

简化版

装饰器模式相比继承更灵活,因为它可以在运行时按需组合多个功能,而继承通常在编译期固定类层级。装饰器用组合替代继承,能减少子类爆炸,也更符合开闭原则。

详细版

继承适合表达稳定的“是什么”关系,例如 Dog extends Animal。但如果用继承表达很多可组合功能,很容易产生类爆炸。

例如一个文本处理器支持:

  • 加密;
  • 压缩;
  • 日志;
  • 缓存;
  • 统计。

用继承可能出现:

EncryptedTextProcessor
CompressedTextProcessor
EncryptedCompressedTextProcessor
CachedEncryptedCompressedTextProcessor
...

装饰器模式可以把每种增强拆成独立装饰器:

TextProcessor processor = new CacheDecorator(
        new CompressDecorator(
                new EncryptDecorator(
                        new PlainTextProcessor()
                )
        )
);

优势:

  • 每个增强独立,职责清楚;
  • 能在运行时决定组合;
  • 新增增强时不改原类;
  • 避免为每种组合创建子类;
  • 更容易复用单个增强能力。

但装饰器也有成本:对象层数更多,调用链更长,调试时要理解组合顺序。所以不是所有继承都要替换成装饰器,只有“功能组合变化多”时才更适合。

完整版教学

一、继承的问题不是继承本身

继承不是坏东西。它适合表达稳定的抽象层级。

例如:

class Dog extends Animal
class Cat extends Animal

这种关系比较自然。

问题出在用继承表达“可选功能组合”。比如饮料加料:

Coffee
MilkCoffee
SugarCoffee
MilkSugarCoffee
IceMilkSugarCoffee

这里 MilkSugarCoffee 并不是一种非常稳定的领域类型,它只是“咖啡 + 牛奶 + 糖”的组合。

当你用继承表达组合时,子类数量会随着功能数量快速增长。

二、组合为什么更灵活

组合的思路是把每个功能做成一个对象,然后运行时拼起来。

Coffee coffee = new SugarDecorator(
        new MilkDecorator(
                new PlainCoffee()
        )
);

这里不需要 MilkSugarCoffee 类。牛奶和糖各自是一个装饰器,它们可以组合,也可以单独使用。

新增“椰奶”时,只需要新增:

class CoconutDecorator extends CoffeeDecorator { ... }

不用修改已有类,也不用新增所有带椰奶组合的子类。

三、装饰器如何体现开闭原则

开闭原则强调:新增功能尽量通过扩展,而不是修改已有代码。

用继承组合功能时,新增一个功能可能要影响很多组合类。

用装饰器时,新增一个功能只新增一个装饰器:

class LogDecorator extends ProcessorDecorator {
    public String process(String input) {
        long start = System.currentTimeMillis();
        try {
            return delegate.process(input);
        } finally {
            System.out.println("cost=" + (System.currentTimeMillis() - start));
        }
    }
}

原来的处理器不用改,其他装饰器也不用改。

这就是对扩展开放、对修改关闭。

四、继承和装饰器的选择边界

如果对象关系稳定,继承更简单。

比如:

abstract class Shape
class Circle extends Shape
class Rectangle extends Shape

这不一定需要装饰器。

如果变化点是功能叠加,装饰器更合适。

比如:

  • 输入流叠加缓冲、解压、解密;
  • 文本处理叠加过滤、格式化、统计;
  • HTTP 客户端叠加重试、日志、签名;
  • UI 组件叠加滚动条、边框、阴影。

判断关键是:这是稳定分类,还是可选能力组合?

五、装饰器也不是没有代价

装饰器的代价包括:

  • 对象层数变多;
  • 调用链不如单个类直观;
  • 顺序错误会导致行为错误;
  • 过度装饰会让代码难读;
  • 调试时需要展开每一层包装。

因此,当功能组合不复杂时,简单继承或直接实现可能更好。

好的设计不是“永远用组合”,而是知道什么时候继承简单,什么时候组合灵活。

六、组合与继承的扩展维度

3 种主体乘 4 种增强,继承组合最坏要 3×2^4=48 个组合类;装饰方案约需 3 个主体类加 4 个装饰类。

inheritance: fixed subclass tree; decorator: runtime object graph

这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。

七、边界、代价与验证

检查维度应确认的内容
正确性“组合优于继承”不是禁用继承,而是避免用继承表达会独立变化的多个维度。
适用边界稳定且天然是 is-a 的核心差异可用继承;需要按实例、按运行时自由组合的附加职责更适合装饰。
测试证据覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序
运行成本记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量

模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。

易错点:“组合优于继承”不是禁用继承,而是避免用继承表达会独立变化的多个维度。

八、常见误区与追问

  • 误区:继承在设计模式中一定是不好的。 继承适合稳定分类和模板骨架;问题在于用它承载大量正交功能组合。
  • 误区:装饰器可以任意改变组件接口。 透明装饰依赖共同的 Component 契约;随意增加只能向下转型调用的方法,会破坏可替换性和继续组合的能力。
  • 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
  • 追问:装饰器是否一定比继承更简单? 组合种类多时更灵活,但对象链、装配和调试也会增加复杂度。
  • 追问:装饰器链越长,扩展性就一定越好吗? 不一定;链越长,顺序、异常传播和排障成本越高,应由装配层集中管理并限制职责粒度。
  • 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。

九、加强记忆

继承适合稳定分类,装饰器适合可选能力组合。只要你发现为了不同功能组合不断创建子类,就该想到装饰器;它用一层层包装替代一堆组合子类。