12.1 Decorator 模式
假如有一塊蛋糕,如果隻塗上奶油,其他什麼也不加,就是奶油蛋糕。如果加上草莓,就是草莓奶油蛋糕。如果再加上一塊黑色巧克力闆,上面用白色巧克力寫上姓名,然後插上蠟燭,就變成了一塊生日蛋糕。
不論是蛋糕、奶油蛋糕、草莓奶油蛋糕,他們的核心都是蛋糕。不過,經過一系列裝飾後,蛋糕的味道變得更加甜美了,目的也變得更加明确了。
程式中的對象與蛋糕十分相似。首先有一個相當于蛋糕的對象,然後不斷地裝飾蛋糕一樣地不斷地對齊增加功能,它就程式設計了使用目的更加明确的對象。
像這樣不斷地為對象添加裝飾的設計模式被稱為 Decorator 模式。
12.2 示例程式
示例程式的功能是給文字添加裝飾邊框。指用 “-”、“\”、“+”、“|” 組成的邊框。
示例程式類圖
|| Display 類
Display 類是可以顯示多行字元串的抽象類。
getColumns 方法和 getRows 方法分别用于擷取橫向字元數和縱向行數。都是抽象方法需要子類實作。getRowText 方法使用者擷取指定的某一行的字元串。
show 是顯示所有行的字元串方法。在show 内部,程式會調用 getRows 方法擷取行數,調用 getRowText 方法擷取改行需要顯示的字元串,然後循環顯示字元串。屬于 Template Method 模式。
public abstract class Display {
public abstract int getColumns(); // 擷取橫向字元數
public abstract int getRows(); // 擷取行數
public abstract String getRowText(int row); // 擷取 row 行的字元串
public void show() { // 列印所有
for (int i = 0; i < getRows(); i++) {
System.out.println(getRowText(i));
}
}
}
|| StringDisplay 類
StringDisplay 類是用于顯示單行字元串的類。是 Display 的子類,是以肩負着實作 Display 類中聲明的抽象方法的重任。
string 字段中儲存的是要顯示的字元串。由于 StringDisplay 類隻顯示一行字元串,是以 getColumns 傳回 string.getBytes().length 的值,getRows 方法傳回固定值1。
僅當要擷取第 0 行時 getRowText 方法才會傳回 string 字段。以本章開頭的蛋糕的比喻來說, StringDisplay 類就相當于生日蛋糕中的核心蛋糕。
/**
* 顯示核心類.
* 隻顯示一行字元串。
*/
public class StringDisplay extends Display{
private String string;
public StringDisplay(String string) {
this.string = string;
}
@Override
public int getColumns() {
return string.length();
}
@Override
public int getRows() {
return 1;
}
@Override
public String getRowText(int row) {
if (row == 0) {
return string;
}
return null;
}
}
|| Border 類
Border 類是裝飾邊框的抽象類。雖然它所表示的是裝飾邊框,但它也是 Display 的子類。
也就是說,通過繼承,裝飾邊框與被裝飾物具有了相同的方法。具體而言,Border 類繼承了父類的各方法。從接口角度而言,裝飾邊框與被裝飾物具有相同的方法也就意味着它們具有一緻性。
在裝飾邊框 Border 類中有一個 Display 類型的 display 字段,它表示裝飾物。不過,其所表示的裝飾物并不僅限于 StringDisplay 的執行個體。因為 Border 也是 Display 的子類,display 字段也可以表示其他的裝飾邊框,而且那個邊框中也有一個 display 字段。
/**
* 裝飾邊框的抽象類.
*/
public abstract class Border extends Display {
protected Display display;
protected Border(Display display) {
this.display = display;
}
}
|| SideBorder 類
SideBorder 類是一種具體的裝飾邊框,是 Border 類的子類。SideBorder 類是指定的字元裝飾字元串的左右兩側。例如,字元為 ‘|’
|被裝飾物|
getColumns 方法是用于擷取橫向字元數的方法。隻需要在被裝飾物的字元數的基礎上,再加上兩側邊框的字元數即可。
1 + display.getColumns() + 1
因為 SideBorder 類并不會在字元串的上下兩側添加字元,是以 getRows 方法直接傳回 display.getRows() 即可。
getRowText 方法可以擷取參數指定的那一行的字元數,是以,像下面這樣,在 display.getRowText(row) 的字元串兩側,加上 borderchar 這個裝飾邊框即可。
borderchar + display.getRowText(row) + borderchar
/**
* 具體的裝飾器-在兩端加上字元
*/
public class SideBorder extends Border {
private char borderChar;
protected SideBorder(Display display, char ch) {
super(display);
this.borderChar = ch;
}
@Override
public int getColumns() {
return 1 + display.getColumns() + 1; // 因為在兩端加上字元,可以表示為 1 + 字元數 + 1
}
@Override
public int getRows() {
return display.getRows(); // 未改變 Row
}
@Override
public String getRowText(int row) {
return borderChar + display.getRowText(row) + borderChar;
}
}
|| FullBorder 類
FullBorder 類與 sideBorder 類相似。其會在字元串的上下左右都加上裝飾邊框。
/**
* 在字元串的上下左右都加上裝飾邊框.
* +-----+
* |Hello|
* +-----+
*/
public class FullBorder extends Border {
public FullBorder(Display display) {
super(display);
}
@Override
public int getColumns() {
return 1 + display.getColumns() + 1;
}
@Override
public int getRows() {
return 1 + display.getRows() + 1;
}
@Override
public String getRowText(int row) {
if (row == 0) {
return '+' + makeLine('-', display.getColumns()) + '+'; // 上邊框
} else if (row == display.getRows() + 1) {
return '+' + makeLine('-', display.getColumns()) + '+'; // 下邊框
} else {
return '|' + display.getRowText(row - 1) + '|'; // 其他邊框
}
}
// 用于連續的顯示某個字元
private String makeLine(char ch, int count) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(ch);
}
return sb.toString();
}
}
|| Main 類
用于測試程式行為的類。b4 用于加多重邊框。
public class Main {
public static void main(String[] args) {
Display b1 = new StringDisplay("Hello, world.");
Display b2 = new SideBorder(b1, '#');
Display b3 = new FullBorder(b2);
b1.show();
b2.show();
b3.show();
Display b4 = new SideBorder(
new FullBorder(
new FullBorder(
new SideBorder(
new FullBorder(
new StringDisplay("Hello, World.")
), '*'
)
)
), '/'
);
b4.show();
}
}
運作結果:
Hello, world.
#Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+
/+-------------------+/
/|+-----------------+|/
/||*+-------------+*||/
/||*|Hello, World.|*||/
/||*+-------------+*||/
/|+-----------------+|/
/+-------------------+/
12.3 Decorator 模式中登場角色
在 Decorator 模式中有以下登場角色。
◆ Component
增加功能時的核心角色。以開頭場景為例,裝飾前的蛋糕就是 Component 角色。Component 角色隻是定義了蛋糕的接口(API)。在示例程式中,由 Display 類扮演此角色。
◆ ConcreteComponent
該角色是實作了 Component 角色所定義的接口的具體蛋糕。在示例程式中,由 StringDisplay 類扮演此角色。
◆ Decorator(裝飾物)
該角色具有與 Component 角色相同的接口。在它内部儲存了被裝飾對象 - Component 角色。Decorator 角色知道自己要裝飾的對象。在示例程式中,由 Border 類扮演此角色。
◆ ConcreteDecorator (具體的裝飾物)
該角色是具體的 Decorator 角色。在示例程式中,由 SideBorder 類和 FullBorder 類扮演此角色。
Decorator 模式的類圖
12.4 拓展思路的要點
|| 接口(API)的透明性
在 Decorator 模式中,裝飾邊框與被裝飾物具有一緻性。具體而言,在示例程式中,表示裝飾邊框的 Border 類是表示被裝飾物的 Display 類的子類,這就展現了它們之間的一緻性。也就是說 Border 類(及其子類)與表示被裝飾物的 Display 類具有相同的接口。
這樣,即使被裝飾物被邊框裝飾起來了,接口(API)也不會被隐藏起來。其他類依然可以調用 getColumns、getRows、getRowText 以及 show 方法。這就是接口(API)的透明性。
得益于接口的透明性,Decorator 模式中也形成了類似于 Composite 模式中的遞歸結構。也就是說,裝飾邊框裡面的 “被裝飾物” 實際上又是别的物體的 “裝飾邊框” 。不過,Decorator 模式雖然與 Composite 模式一樣,都具有遞歸結構,但是它們的使用目的不同。Decorator 模式主要的是通過添加裝飾物來增加對象的功能。
|| 在不改變被裝飾物的前提下增加功能
在 Decorator 模式中,裝飾邊框與被裝飾物具有相同的接口(API)。雖然接口是相同的,但裝飾越多,功能則越多。例如,用 SideBorder 裝飾 Display 後,就可以在字元串的左右兩側加上裝飾字元。如果再用 FullBorder 裝飾,那麼就可以在字元串的四周加上邊框。此時,我們完全不需要對被裝飾的類做任何修改。這樣,我們就實作了不修改被裝飾的類即可增加功能。
|| 可以動态的增加功能
Decorator 模式中用到了委托,它使類之間形成了弱關聯關系。是以,不用改變架構代碼,就可以生成一個與其他對象具有不同關系的新對象。
|| 隻需要一些裝飾物即可添加許多功能
使用 Decorator 模式可以為程式添加許多功能。隻要準備一些裝飾邊框,即使這些裝飾邊框都隻具有非常簡單的功能,也可以将它們自由組合成為新的對象。
就像我們可以自由選擇香草味冰激淩、巧克力冰激淩、草莓味冰激淩一樣。如果冰激淩店要為顧客準備冰激淩成品那就太麻煩了。是以,冰激淩店隻會準備各種香料,當顧客下單後隻需要在冰激淩上加上各種香料就可以了。Decorator 模式就是可以應對這種多功能對象的需求的一種模式。
|| java.io 包與 Decorator 模式
java.io 包是用于輸入輸出的包。在其中,使用了 Decorator 模式。
首先,可以像下面這樣生成一個讀取檔案的執行個體。
Render render = new FileReader("data.txt");
然後,像下面這樣在讀取檔案時将檔案内容放入緩沖區。
Reader reader = new BufferedReader(new FileReader("data.txt"));
這樣,在生成 BufferedReader 類的執行個體時,會指定将檔案讀取到 FileReader 類的執行個體中。
在然後,我們可以想下面這樣管理行号。
Reader reader = new LineNumberReader(new BufferedReader(new FileReader("data.txt")));
可以看出,無論是 LineNumberReader 還是 BufferedReader 類的構造器,都可以接收 Reader 類的執行個體作為參數。
除了 java.io 包以外,還在 javax.swing.border 包中使用了 Decorator 模式。其為我們提供了可為界面中的控件添加裝飾邊框的類。
|| 導緻增加許多很小的類
Decorator 模式的一個缺點就是會導緻程式增加許多功能類似的很小的類。
12.6 延伸閱讀:繼承和委托中的一緻性
我們再稍微了解一下 “一緻性”,即 “可以将不同的東西當作同一種東西看待” 的知識。
|| 繼承-父類與子類的一緻性
子類和父類具有一緻性。如:
class Parent {
...
void parentMethod() {
...
}
}
class Child extends Parent {
...
void childMethod() {
...
}
}
此時,Child 類的執行個體可以儲存在 Parent 類型的變量中,也可以調用從 Parent 類中繼承的方法。也就是說,可以像操作 Parent 類的執行個體一樣操作 Child 類的執行個體。這是将子類當作父類看待。
但是,反過來,将父類當作子類一樣操作,則需要先進行類型轉換(必須是子類的對象)。
Parent obj = new Child();
((Child)obj).childMethod();
|| 委托-自己和被委托對象的一緻性
使用委托讓接口具有透明性,自己和被委托對象具有一緻性。
下面我們來看一個稍微有點生硬的例子。
class Rose {
Violet obj = ...
void method() {
obj.method();
}
}
class Violet {
void method() {
...
}
}
Rose 和 Violet 都有相同的 method 方法。Rose 将 method 方法的處理委托給了 Violet。這樣會讓人有一種好像這兩個類有所關聯,又好像沒有關聯的感覺。
因為這兩個類雖然都有 method 方法,但是卻沒有明确地在代碼中提現出這個 “共通性”。如果要明确地表示 method 方法是共通的,隻需要像下面這樣編寫一個共通的抽象類 Flower 就可以了。
abstract class Flower {
abstract void method();
}
class Rose extends Flower {
Violet obj = ...
void method() {
obj.method();
}
}
class Violet extends Flower {
void method() {
...
}
}
或是将 Flower 作為接口也行。
interface Flower {
void method();
}
至此,可能會産生這樣的疑問,即 Rose 類中的 Obj 字段被指定為具體的 Violet 真的好嗎?如果指定為抽象類型 Flower 會不會更好呢?究竟應該怎麼做才好,其實沒有固定的答案,需求不同,做法也會不同。