什么是透明装饰器和半透明装饰器?
简化版
透明装饰器只暴露组件接口,客户端可以把装饰器当成原对象使用;半透明装饰器会在装饰器类中新增方法,功能更强但透明性变弱。一般优先保持透明,只有确实需要额外能力时才考虑半透明。
详细版
透明装饰器要求装饰器和被装饰对象完全遵循同一个接口。
interface Component {
void operation();
}
class ConcreteDecorator implements Component {
private final Component delegate;
public void operation() {
delegate.operation();
extra();
}
}
客户端只依赖 Component:
Component component = new ConcreteDecorator(new ConcreteComponent());
component.operation();
半透明装饰器除了实现组件接口,还提供额外方法:
class ReportDecorator implements Report {
public void export() { ... }
public void exportWithWatermark() { ... } // 新增方法
}
如果客户端想调用新增方法,就必须依赖具体装饰器类型:
ReportDecorator decorator = new ReportDecorator(report);
decorator.exportWithWatermark();
这会降低透明性和可替换性。
透明装饰器更符合装饰器模式的经典思想;半透明装饰器在工程里也可能出现,但要控制使用边界。
完整版教学
一、什么叫透明
透明的意思是:客户端不需要知道自己拿到的是原始对象还是装饰器。
例如:
InputStream in = new BufferedInputStream(new FileInputStream(file));
调用方只按 InputStream 使用:
int b = in.read();
它不需要关心 in 到底是 FileInputStream,还是 BufferedInputStream,还是更多层包装。
这种设计让装饰器可以透明替换原对象。
二、透明装饰器的好处
透明装饰器有几个明显优势:
- 客户端只依赖抽象接口;
- 装饰器和真实对象可以互相替换;
- 多层装饰可以继续组合;
- 调用方不需要判断具体类型;
- 更符合开闭原则和里氏替换原则。
比如:
DataSource source = new EncryptedDataSource(new FileDataSource());
save(source);
save() 方法只接收 DataSource:
void save(DataSource source) {
source.write("data");
}
这样后续加不加压缩、缓存、日志,都不影响 save()。
三、什么叫半透明
半透明装饰器会新增接口之外的方法。
例如:
class ScrollDecorator implements Component {
public void draw() { ... }
public void scrollTo(int y) { ... }
}
draw() 是组件接口方法,scrollTo() 是装饰器新增能力。
如果客户端只用 Component 引用:
Component c = new ScrollDecorator(panel);
它无法调用 scrollTo()。
如果要调用,就必须写成:
ScrollDecorator c = new ScrollDecorator(panel);
c.scrollTo(100);
这样客户端就知道了具体装饰器,透明性下降。
四、半透明装饰器是不是一定不好
不一定。
有些场景确实需要装饰器提供额外操作。例如 UI 滚动装饰器可能需要暴露滚动位置控制,调试装饰器可能需要暴露统计信息。
但要明白代价:
- 客户端和具体装饰器耦合;
- 后续替换装饰器更难;
- 多层装饰时新增方法不一定容易访问;
- 抽象接口的统一性变弱。
因此半透明装饰器应该谨慎使用。如果新增方法是所有组件都应该具备的能力,可能应该把它提升到公共接口;如果只是某个装饰器的内部能力,尽量不要暴露给普通业务代码。
五、面试中怎么回答
可以这样回答:
透明装饰器完全遵循组件接口,客户端不感知装饰器存在,扩展性和可替换性最好;半透明装饰器在装饰器中增加了新方法,增强能力更直接,但客户端需要依赖具体装饰器,破坏一定透明性。工程中优先透明,必要时半透明,但要控制耦合。
这个回答同时讲了定义、优缺点和取舍。
六、可替换性与专有能力的取舍
若 5 层链都只暴露 Component,客户端只需依赖 1 个接口;若第 3 层新增 resize(),调用者要拿到该具体类型,链的透明性被打破。
transparent: client -> Component; semi-transparent: client -> SpecificDecorator API
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 透明性越强,任意组合越容易;专有能力越多,表达力提高但替换与重排更困难。 |
| 适用边界 | 半透明不是错误,它适合确实需要专有操作的场景,但应承认调用方与具体装饰器产生了耦合。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:透明性越强,任意组合越容易;专有能力越多,表达力提高但替换与重排更困难。
八、常见误区与追问
- 误区:半透明装饰器仍能完全隐藏具体类型。 一旦调用专有方法,客户端就必须知道具体装饰器或额外接口。
- 误区:装饰器可以任意改变组件接口。 透明装饰依赖共同的 Component 契约;随意增加只能向下转型调用的方法,会破坏可替换性和继续组合的能力。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:如何兼顾专有能力与组合? 可把能力拆成独立小接口,让调用方依赖能力接口,而不是直接依赖具体类。
- 追问:装饰器链越长,扩展性就一定越好吗? 不一定;链越长,顺序、异常传播和排障成本越高,应由装配层集中管理并限制职责粒度。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
透明装饰器让客户端只看见接口,半透明装饰器让客户端看见具体装饰器。透明更优雅,半透明更直接;能用统一接口解决就别暴露额外方法。