Decorator(裝飾者)用於動態擴展物件職責,不改變被裝飾物件本身屬性。生活例子:相框裝飾畫作、化妝品裝飾臉龐。Java 經典案例:
JScrollPane 裝飾 JComponent → 新增可捲動屬性。BufferedInputStream 裝飾 InputStream → 新增緩衝讀取加速。DebugButton 裝飾 JButton → 點擊時記錄日誌。ScrollOverButton 裝飾 JButton → 滑鼠經過顯示凸起邊框。public class DebugButton extends JButton {
public DebugButton() {
addActionListener(e -> System.out.println("debug message"));
}
}
// ScrollOverButton 同理,加入 MouseListener 切換邊框
public class ScrollOverButton extends JButton {
public ScrollOverButton() {
addMouseListener(new MouseAdapter() {
@Override public void mouseEntered(MouseEvent e) { setBorder(BorderFactory.createRaisedBevelBorder()); }
@Override public void mouseExited(MouseEvent e) { setBorder(BorderFactory.createEmptyBorder()); }
});
}
}
優點:直觀、保留原類型(DebugButton instanceof JButton 為 true)、幾乎能實作所有裝飾。
缺點:
ScrollOverDebugButton;若有 N 種行為,理論上需 2^N 個子類別。setDebug()/isDebug() 等)。核心思想:持有被裝飾物件參考,轉發請求並在前後加入新行為。
// BufferedInputStream 經典封裝實作(簡化)
public class BufferedInputStream extends InputStream {
private InputStream in; // 被裝飾物件
private byte[] buf; // 緩衝區
public BufferedInputStream(InputStream in) { this.in = in; }
@Override public int read() throws IOException {
// 先讀緩衝區,空則 in.read() 填充
}
}
// JScrollPane 封裝視圖組件
// DebugButton 封裝實作
public class DebugButton extends JButton { // 仍繼承 JButton 以保型別
private JButton delegate;
public DebugButton(JButton delegate) { this.delegate = delegate; }
// 轉發所有方法給 delegate,並在 actionPerformed 前加日誌
}
優點:執行期動態組合(new DebugButton(new ScrollOverButton(new JButton())))、符合開放/封閉原則、無組合爆炸。
缺點:型別身分喪失——JScrollPane 不能替換內部視圖組件(getFont() 返回自身字體而非視圖字體),instanceof 檢查失效。
將裝飾邏輯完全外部化,透過事件監聽、屬性映射、動態代理或 AOP 框架(Spring AOP、AspectJ)在不修改原類別、不建立子類別情況下注入行為。例如:
JButton 註冊全域 ActionListener 記錄日誌。UIManager 設定全域 ButtonUI 實現滑鼠經過效果。優點:零侵入、關注點分離、大範圍統一套用。
缺點:需框架支援、除錯較難、效能微額開銷。
| 場景 | 推薦策略 |
|---|---|
| 少數固定裝飾、需保留型別 | 繼承 |
| 動態組合、行為正交、不需 instanceof | 封裝 |
| 橫切關注點(日誌、事務、安全)、大量同類元件 | 外掛/AOP |
Java I/O 庫(BufferedInputStream、GZIPInputStream)採封裝;Swing 邊框/UI 委派採外掛;簡單 UI 擴展常用繼承。實務中常混合使用。