你確定理解裝飾器設計模式?
1 簡介
一般有兩種方式給一個類或對象新增行爲:
-
• 繼承 子類在擁有自身方法同時還擁有父類方法。但這種是靜態的,用戶無法控制增加行爲的方式和時機
-
• 關聯 將一個類的對象嵌入另一個對象,由另一個對象決定是否調用嵌入對象的行爲以便擴展自身行爲,這個嵌入的對象就叫做裝飾器 (Decorator)
2 定義
結構型模式。
動態給一個對象增加額外功能,裝飾器模式比生成子類實現更爲靈活。裝飾模式以對用戶透明的方式動態給一個對象附加功能。用戶不會覺得對象在裝飾前、後有何不同。裝飾模式可在無需創造更多子類情況下,擴展對象功能。
3 架構
-
• Component 接口: 抽象構件 定義了對象的接口,可以給這些對象動態增加功能
-
• ConcreteComponent 具體類: 具體構件 定義了具體的構件對象,實現了 在抽象構件中聲明的方法,裝飾器可以給它增加額外的職責(方法)
-
• Decorator 抽象類: 裝飾類 抽象裝飾類是抽象構件類的子類,用於給具體構件增加職責,但是具 體職責在其子類中實現;
-
• ConcreteDecorator 具體類: 具體裝飾類 具體裝飾類是抽象裝飾類的子類,負責向構 件添加新的職責。
圖片
4 案例
4.1 噁心的 Java I/O 類庫
Java I/O 類庫幾十個類,負責 I/O 數據的讀取和寫入:
針對不同讀取和寫入場景,又在這四個父類基礎之上,擴展子類:
比如打開文件 test.txt,從中讀取數據。InputStream 是抽象類:
-
• FileInputStream,專用讀取文件流的子類
-
• BufferedInputStream,支持帶緩存功能的數據讀取類,可以提高數據讀取的效率。
InputStream in = new FileInputStream("/user/javaedge/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}
看着還真麻煩,得先創建一個 FileInputStream,再傳給 BufferedInputStream 使用。我在想,爲何不直接設計個繼承 FileInputStream 且支持緩存的 BufferedFileInputStream?這樣就能直接創建一個 BufferedFileInputStream 對象,打開文件讀取數據,用着不是很簡單嗎?
InputStream bin = new BufferedFileInputStream("/user/javaegde/test.txt");
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}
4.1.1 繼承設計
若 InputStream 只有一個子類 FileInputStream,在 FileInputStream 基礎上,再設計個孫類 BufferedFileInputStream,也能接受,畢竟繼承結構簡單。但實際上,繼承 InputStream 的子類有很多。需要給每個 InputStream 子類,再繼續派生支持緩存讀取的子類。
除了支持緩存讀取,若還需增強其它功能,如 DataInputStream 支持按基本數據類型讀取數據:
FileInputStream in = new FileInputStream("/user/wangzheng/test.txt");
DataInputStream din = new DataInputStream(in);
int data = din.readInt();
這樣,若繼續使用繼承,就需再派生 DataFileInputStream、DataPipedInputStream 等類。若還需既支持緩存、又支持基本類型讀取數據的類,就得再繼續派生 BufferedDataFileInputStream、BufferedDataPipedInputStream 等 n 多類。這還只是附加了倆功能,若還需附加更多增強功能,就會導致組合爆炸,類繼承結構無比複雜,代碼既不好擴展,也不好維護。
4.1.2 裝飾器模式設計
“組合優於繼承”,針對這種繼承複雜問題,通過改爲組合關係即可解決:
public abstract class InputStream {
//...
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
//...
}
public long skip(long n) throws IOException {
//...
}
public int available() throws IOException {
return 0;
}
public void close() throws IOException {}
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
public boolean markSupported() {
return false;
}
}
public class BufferedInputStream extends InputStream {
protected volatile InputStream in;
protected BufferedInputStream(InputStream in) {
this.in = in;
}
//...實現基於緩存的讀數據接口...
}
public class DataInputStream extends InputStream {
protected volatile InputStream in;
protected DataInputStream(InputStream in) {
this.in = in;
}
//...實現讀取基本類型數據的接口
}
裝飾器模式就是簡單的 “用組合替代繼承”?當然不是啦。從 IO 案例看,裝飾器模式相比簡單的組合關係,還有兩個特殊點:
- • 裝飾器類和原始類繼承同樣的父類,這樣我們可以對原始類 “嵌套” 多個裝飾器類如對 FileInputStream 嵌套倆裝飾器類:BufferedInputStream 和 DataInputStream,讓它支持緩存、按基本數據類型讀取數據。
InputStream in = new FileInputStream("/user/javaedge/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();
- • 裝飾器類是對功能的增強,這也是裝飾器模式應用場景的一個重要特點其實符合 “組合關係” 的設計模式很多,如代理模式、橋接模式。儘管代碼結構相似,但意圖不同。比較相似的代理模式和裝飾器模式,代理模式中,代理類附加的是跟原始類無關功能,裝飾器模式中,裝飾器類附加的是跟原始類相關的增強功能。
// 代理模式的代碼結構(下面的接口也可以替換成抽象類)
public interface IA {
void f();
}
public class A impelements IA {
public void f() { //... }
}
public class AProxy impements IA {
private IA a;
public AProxy(IA a) {
this.a = a;
}
public void f() {
// 新添加的代理邏輯
a.f();
// 新添加的代理邏輯
}
}
// 裝飾器模式的代碼結構(下面的接口也可以替換成抽象類)
public interface IA {
void f();
}
public class A impelements IA {
public void f() { //... }
}
public class ADecorator impements IA {
private IA a;
public ADecorator(IA a) {
this.a = a;
}
public void f() {
// 功能增強代碼
a.f();
// 功能增強代碼
}
}
BufferedInputStream、DataInputStream 並非繼承自 InputStream,而是 FilterInputStream。這是啥設計原則,要引入這個類?
InputStream 是抽象類而非接口,大部分函數(如 read()、available())都有默認實現,按理只需在 BufferedInputStream 類中重新實現那些需要增加緩存功能的函數,其他函數繼承 InputStream 的默認實現。但這行不通。
即使無需增加緩存功能的函數,BufferedInputStream 也得將其重新實現,簡單封裝對 InputStream 對象的函數調用:若不重新實現,BufferedInputStream 就無法將最終讀取數據的任務,委託給傳遞進來的 InputStream 對象
public class BufferedInputStream extends InputStream {
protected volatile InputStream in;
protected BufferedInputStream(InputStream in) {
this.in = in;
}
// f()函數無需增強,只是重新調用InputStream in對象的f()
public void f() {
in.f();
}
}
DataInputStream 也有這種問題。爲避免代碼重複,Java IO 抽象出一個裝飾器父類 FilterInputStream:InputStream 的所有裝飾器類(BufferedInputStream、DataInputStream)都繼承自該裝飾器父類。這樣,裝飾器類只需實現它需增強的方法,其他方法繼承裝飾器父類的默認實現。
public class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
return in.read(b, off, len);
}
public long skip(long n) throws IOException {
return in.skip(n);
}
public int available() throws IOException {
return in.available();
}
public void close() throws IOException {
in.close();
}
public synchronized void mark(int readlimit) {
in.mark(readlimit);
}
public synchronized void reset() throws IOException {
in.reset();
}
public boolean markSupported() {
return in.markSupported();
}
}
4.2 窗口
窗口接口
public interface Window {
// 繪製窗口
public void draw();
// 返回窗口的描述
public String getDescription();
}
無滾動條功能的簡單窗口實現
public class SimpleWindow implements Window {
@Override
public void draw() {
// 繪製窗口
}
@Override
public String getDescription() {
return "simple window";
}
}
以下類包含所有 Window 類的 decorator,以及修飾類本身。
// 抽象裝飾類 注意實現Window接口
public abstract class WindowDecorator implements Window {
// 被裝飾的Window
protected Window decoratedWindow;
public WindowDecorator (Window decoratedWindow) {
this.decoratedWindow = decoratedWindow;
}
@Override
public void draw() {
decoratedWindow.draw();
}
@Override
public String getDescription() {
return decoratedWindow.getDescription();
}
}
// 第一個具體裝飾器 添加垂直滾動條功能
public class VerticalScrollBar extends WindowDecorator {
public VerticalScrollBar(Window windowToBeDecorated) {
super(windowToBeDecorated);
}
@Override
public void draw() {
super.draw();
drawVerticalScrollBar();
}
private void drawVerticalScrollBar() {
// Draw the vertical scrollbar
}
@Override
public String getDescription() {
return super.getDescription() + ", including vertical scrollbars";
}
}
// 第二個具體裝飾器 添加水平滾動條功能
public class HorizontalScrollBar extends WindowDecorator {
public HorizontalScrollBar (Window windowToBeDecorated) {
super(windowToBeDecorated);
}
@Override
public void draw() {
super.draw();
drawHorizontalScrollBar();
}
private void drawHorizontalScrollBar() {
// Draw the horizontal scrollbar
}
@Override
public String getDescription() {
return super.getDescription() + ", including horizontal scrollbars";
}
}
4.3 mybatis
5 優點
使用裝飾模式來實現擴展比繼承更加靈活,它以對客戶透明的方式動態地給一個對象附加更多的責任。裝飾模式可以在不需要創造更多子類的情況下,將對象的功能加以擴展。
與繼承相比,關聯關係的優勢在於不破壞類的封裝性,而且繼承是一種耦合度較大的靜態關係,無法在程序運行時動態擴展。可通過動態方式擴展一個對象的功能,通過配置文件可以在運行時選擇不同裝飾器,從而實現不同行爲。
在軟件開發階段,關聯關係雖然不會比繼承關係減少編碼量,但到了軟件維護階段,由於關聯關係使系統具有較好的低耦合性,所以更容易維護。
通過使用不同具體裝飾類以及這些裝飾類的排列組合,可以創造出很多不同行爲的組合。可以使用多個具體裝飾類來裝飾同一對象,得到功能更強大的對象。
具體構件類與具體裝飾類可以獨立變化,用戶可以根據需要增加新的具體構件類、具體裝飾類,在使用時再對其進行組合,原有代碼無須改變,符合 “開閉原則”。
6 缺點
產生很多小對象,這些對象區別在於它們之間相互連接的方式不同,而不是它們的類或屬性值不同,同時還將產生很多具體裝飾類。這些裝飾類和小對象的產生將增加系統的複雜度,加大學習與理解的難度。
比繼承更靈活,也意味着比繼承更易出錯,排查也更困難,對於多次裝飾的對象,調試時尋找錯誤可能需要逐級排查,較爲煩瑣。
7 適用場景
在不影響其他對象的情況下,以動態、透明的方式給單個對象添加職責。需要動態地給一個對象增加功能,這些功能也可以動態地被撤銷。當不能採用繼承的方式對系統進行擴充或者採用繼承不利於系統擴展和維護時。
不能採用繼承的場景:
-
• 系統存在大量獨立擴展,爲支持每一種組合將產生大量的子類,使得子類數目呈爆炸性增長
-
• 類定義不能繼承(如 final 類)
V.S 代理模式
對於添加緩存這個場景,使用哪種模式,要看設計者意圖:
-
• 設計者不需要用戶關注是否使用了緩存功能,要隱藏實現細節,即用戶只能看到和使用代理類,那就使用代理模式
-
• 反之,若設計者需要用戶自己決定是否使用緩存功能,需要用戶自己新建原始對象並動態添加緩存功能,就使用 decorator 模式
代理模式和裝飾者模式都是代碼增強:
-
• 前者偏重業務無關,高度抽象,和穩定性較高的場景(性能其實可以拋開不談)
-
• 後者偏重業務相關,定製化訴求高,改動較頻繁的場景
緩存一般都是高度抽象,全業務通用,基本不會改動,所以一般也是採用代理模式,讓業務開發從緩存代碼的重複勞動中解放。但若當前業務的緩存實現需要特殊化定製,需揉入業務屬性,就該採用裝飾者模式。因其定製性強,其他業務也用不着,而且業務是頻繁變動的,所以改動的可能也大,相對於動代,裝飾者在調整(修改和重組)代碼這件事上顯得更靈活。
8 擴展
一個裝飾類的接口必須與被裝飾類的接口保持相同,對於客戶端來說無論是裝飾之前的對象還是裝飾之後的對象都可以一致對待。儘量保持具體構件類的輕量,也就是說不要把太多的邏輯和狀態放在具體構件類中,可以通過裝飾類對其進行擴展。
裝飾模式分類:
-
• 透明裝飾模式 要求客戶端完全針對抽象編程,裝飾模式的透明性要求客戶端程序不應該聲明具體構件類型和具體裝飾類型,而應該全部聲明爲抽象構件類型
-
• 半透明裝飾模式 允許用戶在客戶端聲明具體裝飾者類型的對象,調用在具體裝飾者中新增的方法
9 總結
裝飾器模式主要解決繼承複雜的問題,使用組合替代繼承,來給原始類添加增強功能。
裝飾器模式還有一個特點,能對原始類嵌套使用多個裝飾器。爲滿足該應用場景,在設計時,裝飾器類需要跟原始類繼承相同的抽象類或接口。
參考
- • https://zh.wikipedia.org/wiki/%E4%BF%AE%E9%A5%B0%E6%A8%A1%E5%BC%8F#/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Fq-P4B5eZzBc1EygbrqL2g