Java設計模式-裝飾者模式
著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。
定義
裝飾者模式:在不改變原類檔案以及不使用繼承的情況下,動態地將責任附加到物件上,從而實現動態拓展一個物件的功能。它是通過建立一個包裝物件,也就是裝飾來包裹真實的物件。
原則
要使用裝飾者模式,需要滿足以下設計原則:
- 多用組合,少用繼承
- 開放-關閉原則:類應該對拓展開放,對修改關閉
UML類圖
我們先來看看裝飾者模式的類圖,再來詳細講述:
由上自下:-
Component
是基類。通常是一個抽象類或者一個介面,定義了屬性或者方法,方法的實現可以由子類實現或者自己實現。通常不會直接使用該類,而是通過繼承該類來實現特定的功能,它約束了整個繼承樹的行為。比如說,如果Component
-
ConcreteComponen
是Component
的子類,實現了相應的方法,它充當了“被裝飾者”的角色。 -
Decorator
也是Component
的子類,它是裝飾者共同實現的抽象類(也可以是介面)。比如說,Decorator
代表衣服這一類裝飾者,那麼它的子類應該是T恤、裙子這樣的具體的裝飾者。 -
ConcreteDecorator
是Decorator
的子類,是具體的裝飾者,由於它同時也是Component
的子類,因此它能方便地拓展Component
的狀態(比如新增新的方法)。每個裝飾者都應該有一個例項變數用以儲存某個Component
的引用,這也是利用了組合的特性。在持有Component的引用後,由於其自身也是Component
ConcreteDecorator
包裹了Component
,不但有Component
的特性,同時自身也可以有別的特性,也就是所謂的裝飾。
例項演示
為了更加深刻地理解裝飾者模式,我們來看一個簡單的栗子。首先,我們假設現在有這樣一個需求:你有一家服裝店,賣各式各樣的衣服,現在需要用一個系統來記錄客戶所要購買的衣服的總價,以便方便地結算。那麼在這個例子裡面,我們可以用裝飾者模式,把客戶當做被裝飾者,衣服是裝飾者,這很直觀形象吧,接著我們來一步步實現需求。
建立Component基類
因為總體物件是人,所以我們可以把人抽象為基類,新建Person.java:
public abstract class Person {
String description = "Unkonwn" ;
public String getDescription()
{
return description;
}
public abstract double cost(); //子類應該實現的方法
}
複製程式碼
建立被裝飾者——ConcreteComponent
客戶分為很多種,有兒童、青少年、成年人等,因此我們可以建立不同的被裝飾者,這裡我們建立青少年的被裝飾者,新建Teenager.java:
public class Teenager extends Person {
public Teenager() {
this.description = "Shopping List:";
}
@Override
public double cost() {
//什麼都沒買,不用錢
return 0;
}
}
複製程式碼
建立Decorator
由於不同的部位有不同的衣物,不能混為一談,比如說,衣服、帽子、鞋子等,那麼這裡我們建立的Decorator為衣服和帽子,分別新建ClothingDecorator.java
和HatDecorator.java
:
public abstract class ClothingDecorator extends Person {
public abstract String getDescription();
}
複製程式碼
public abstract class HatDecorator extends Person {
public abstract String getDescription();
}
複製程式碼
建立ConcreteDecorator
上面既然已經建立了兩種Decorator
,那麼我們基於它們進行拓展,創建出不同的裝飾者,對於Clothing
,我們新建Shirt.java
,對於Hat,我們新建Casquette
,其實可以根據不同型別的衣物建立更多不同的裝飾者,這裡只是作為演示而建立了兩種。程式碼如下所示:
public class Shirt extends ClothingDecorator {
//用例項變數儲存Person的引用
Person person;
public Shirt(Person person)
{
this.person = person;
}
@Override
public String getDescription() {
return person.getDescription() + "a shirt ";
}
@Override
public double cost() {
return 100 + person.cost(); //實現了cost()方法,並呼叫了person的cost()方法,目的是獲得所有累加值
}
}
複製程式碼
public class Casquette extends HatDecorator {
Person person;
public Casquette(Person person) {
this.person = person;
}
@Override
public String getDescription() {
return person.getDescription() + "a casquette "; //鴨舌帽
}
@Override
public double cost() {
return 75 + person.cost();
}
}
複製程式碼
最後我們在測試類測試我們的程式碼:
public class Shopping {
public static void main(String[] args) {
Person person = new Teenager();
person = new Shirt(person);
person = new Casquette(person);
System.out.println(person.getDescription() + " ¥ " +person.cost());
}
}
複製程式碼
先建立一個Teenager
物件,接著用Shirt
裝飾它,就變成了穿著Shirt
的Teenager
,再用Casquette
裝飾,就變成了戴著Casquette
的穿著Shirt
的Teenager
。執行結果如下所示:
Teenager、Shirt、Casquette
都是繼承自Person
基類,但是具體實現不同,Teenager
是Person
的直接子類,表示了被裝飾者;Teenager、Shirt
是裝飾者,儲存了Person
的引用,實現了cost()
方法,並且在cost()
方法內部,不但實現了自己的邏輯,同時也呼叫了Person
引用的cost()
方法,即獲取了被裝飾者的資訊,這是裝飾者的一個特點,儲存引用的目的就是為了獲取被裝飾者的狀態資訊,以便將自身的特性加以組合。
總結
以上就是裝飾者模式的一個小栗子,講述了裝飾者的基本用法。總結一下裝飾者模式的特點。
- 裝飾者和被裝飾者有相同的介面(或有相同的父類)。
- 裝飾者儲存了一個被裝飾者的引用。
- 裝飾者接受所有客戶端的請求,並且這些請求最終都會返回給被裝飾者(參見韋恩圖)。
- 在執行時動態地為物件新增屬性,不必改變物件的結構。
使用裝飾者模式的最大好處就是其拓展性十分良好,通過使用不同的裝飾類來使得物件具有多種多樣的屬性,靈活性比直接繼承好。然而它也有缺點,那就是會出現很多小類,即裝飾類,使程式變得複雜。
擴充套件閱讀
學習了裝飾者模式用法、特點以及優缺點後,我們再來看看裝飾者模式在實際開發過程的應用。裝飾者模式在Java中經常出現的地方就是JavaIO。提到JavaIO,腦海中就冒出了大量的類:InputStream、FileInputStream、BufferedInputStream
……等,真是頭都大了,其實,這裡面大部分都是裝飾類,只要弄清楚這一點就容易理解了。我們來看看JavaIO是怎樣使用裝飾者模式的。
從字元流來分析,我們知道,有兩個基類,分別是InputStream
和OutputStream
,它們也就是我們上面所述的Component
基類。接著,它有如下子類:FileInputStream、StringBufferInputStream
等,它們就代表了上面所述的ConcreteComponent
,即裝飾物件。此外,InputStream
還有FilterInputStream
這個子類,它就是一個抽象裝飾者,即Decorator
,那麼它的子類:BufferedInputStream、DataInputStream
等就是具體的裝飾者了。那麼,從裝飾者模式的角度來看JavaIO,是不是更加容易理解了呢?
下面,我們來自己實現自己的JavaIO的裝飾者。要實現的功能是:把一段話裡面的每個單詞的首字母大寫。
我們先新建一個類:UpperFirstWordInputStream.java
public class UpperFirstWordInputStream extends FilterInputStream {
private int cBefore = 32;
protected UpperFirstWordInputStream(InputStream in) {
//由於FilterInputStream已經儲存了裝飾物件的引用,這裡直接呼叫super即可
super(in);
}
public int read() throws IOException{
//根據前一個字元是否是空格來判斷是否要大寫
int c = super.read();
if(cBefore == 32)
{
cBefore = c;
return (c == -1 ? c: Character.toUpperCase((char) c));
}else{
cBefore = c;
return c;
}
}
}
複製程式碼
接著編寫一個測試類:InputTest.java
public class InputTest {
public static void main(String[] args) throws IOException {
int c;
StringBuffer sb = new StringBuffer();
try {
//這裡用了兩個裝飾者,分別是BufferedInputStream和我們的UpperFirstWordInputStream
InputStream in = new UpperFirstWordInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
while((c = in.read()) >= 0)
{
sb.append((char) c);
}
System.out.println(sb);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
複製程式碼
(注意:上面的test.txt檔案需要你自行建立,放到同一個資料夾內即可,內容可隨意填寫。) 最後,我們看下執行結果: