在本講,我們來學習一下結構型模式里面的第三個設計模式,即裝飾者模式,
概述
在學習裝飾者模式之前,我們先來看一個快餐店的例子,
快餐店有炒面、炒飯這些快餐,可以額外附加雞蛋、火腿、培根這些配菜,當然加配菜需要額外加錢,每個配菜的價錢通常不太一樣,那么這樣計算總價就會顯得比較麻煩,比如說客戶點一份炒面,他還要再加一個雞蛋和一根火腿,那么計算總價的時候就會很麻煩,
假設我們現在要設計這么一個計算總價的系統的話,那么用傳統的方式應該如何去做呢?是不是立馬想到了要用繼承的方式去做啊!這時,所設計出來的類圖就應該是下面這個樣子的,

最上面的是快餐類,它里面有兩個成員變數,一個是price(即價格),一個是desc(即描述,例如炒飯的描述就是炒飯,炒面的描述就是炒面),當然還為它們提供了對應的getter和setter方法,此外,該快餐類里面還有一個cost方法,它就是用來計算快餐總價格的,
然后,快餐類又有兩個子類,一個是炒飯類(即FiredRice),一個是炒面類(即FiredNoodles),它倆都有各自對應的無參構造,除此之外,它倆都重寫了父類中的cost方法,以計算總價格,
由于對于炒飯和炒面來說,它們都可以加雞蛋或者培根,所以我就又為它們設計了不同的子類,對于炒飯類來說,它有兩個子類,分別是加雞蛋的炒飯類(即EggFriedRice)和加培根的炒飯類(即BaconFriedRice);對于炒面類來說,它也有兩個子類,分別是加雞蛋的炒面類(即EggFriedNoodles)和加培根的炒面類(即BaconFriedNoodles),
以上就是我們用傳統繼承的方式來實作咱們快餐店的案例,
那么使用繼承方式去實作的話,會存在一個什么樣的問題呢?使用繼承的方式所存在的問題:
-
擴展性不好
如果要再加一種配料(比如火腿腸),那么我們就會發現需要給FriedRice和FriedNoodles分別定義一個子類,如果要新增一個快餐品類(比如炒河粉)的話,那么就需要定義更多的子類了,為什么這么說呢?假設我們在以上類圖中新添加了一個炒河粉的子類,那么它肯定是要繼承自快餐類的,此時,它下面就要有三個子類了,分別是加雞蛋的炒河粉類、加培根的炒河粉類以及加火腿腸的炒河粉類,你會發現類爆炸的情況就出現了
-
產生過多的子類
問題既然出現了,那么我們應該如何對上面的快餐店案例進行一個改進呢?此時,我們就可以使用裝飾者模式了,那裝飾者模式到底是什么呢?下面我們就來看看它的概念,
裝飾者模式是指在不改變現有物件結構的情況下,動態地給該物件增加一些職責(即增加其額外功能)的模式,
在以上快餐店案例中,對于炒飯來說,給其增加一個額外的職責,其實就是給其加一個雞蛋或者培根或者火腿腸,因此,對于該案例而言,我們就可以使用裝飾者模式了,
結構
裝飾者(Decorator)模式中的角色有如下四個:
- 抽象構件(Component)角色:定義一個抽象介面以規范準備接收附加責任的物件,注意,此處的抽象介面既可以是介面也可以是抽象類,該角色對應以上快餐店案例中的快餐類
- 具體構件(ConcreteComponent)角色:實作抽象構件,通過裝飾角色為其添加一些職責,例如,以上快餐店案例中的炒飯類、炒面類都屬于具體構建角色
- 抽象裝飾(Decorator)角色:繼承或實作抽象構件,并包含具體構件的實體(也就是說將其聚合進來了),可以通過其子類擴展具體構件的功能,所以,裝飾者模式巧妙就巧妙在這個位置
- 具體裝飾(ConcreteDecorator)角色:實作抽象裝飾的相關方法,并給具體構件物件添加附加的責任
裝飾者模式案例
上面我們學習了裝飾者模式的概念,以及知道了它里面所具有的角色,接下來,我們就使用裝飾者模式來對以上快餐店案例進行一個改進,以此體會裝飾者模式的精髓,
分析
咱們先看下下面的這張類圖,

首先,我們應該明確要有一個快餐類,而且它還要是一個抽象類,它里面有兩個成員變數,一個是price(即價格),一個是desc(即描述),當然還為它們提供了對應的getter和setter方法,此外,該快餐類里面還有一個cost方法,它就是用來計算快餐總價格的,當然該方法是抽象的,需要由子類具體來實作,
然后,快餐類又有兩個子類,一個是炒飯類(即FiredRice),一個是炒面類(即FiredNoodles),它倆都有各自對應的無參構造,除此之外,它倆都重寫了父類中的cost方法,以計算總價格,
以上類圖的左半邊部分我們分析完了之后,再來分析一下右半邊部分,
可以看到有一個Garnish類,它是一個核心類,即裝飾者,作為裝飾者,首先它要去繼承快餐類(即FastFood),并又聚合該類的物件,這樣,我們就得為其提供一個有參構造和相應的getter、setter方法了,
接著,咱們的配料類,比如Egg、Bacon,就得提供對應的有參構造給父類(即Garnish)中的FastFood物件進行賦值了,并且還得重寫父類中的cost方法和getDesc方法,重寫父類中的cost方法好理解,就是為了計算總價,那為啥還要重寫父類中的getDesc方法呢?這是因為如果某個快餐加了雞蛋(或者培根)的話,那么它的描述肯定是不同了,所以我們就得重寫父類中的getDesc方法來獲取最終的一個描述了,
經過以上分析,大家一定要清楚,Garnish是最核心的一個類,它不僅繼承了FastFood類還聚合了FastFood類,
分析完了以后,接下來,我們就要開始撰寫代碼實作以上案例了,
實作
首先,打開咱們的maven工程,并在com.meimeixia.pattern包下新建一個子包,即decorator,也即裝飾者模式的具體代碼我們是放在了該包下,
然后,創建快餐類,這里我們命名為FastFood,注意,該類是一個抽象類,
package com.meimeixia.pattern.decorator;
/**
* 快餐類(抽象構件角色)
* @author liayun
* @create 2021-07-31 13:02
*/
public abstract class FastFood {
private float price; // 價格
private String desc; // 描述
public FastFood(float price, String desc) {
this.price = price;
this.desc = desc;
}
public FastFood() {
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
/*
* 計算總價,注意,該方法是一個抽象的方法,這是因為只有我們知道了具體的快餐之后,才能計算出來它的價格,
* 例如,如果客戶點的是炒飯,而一碗炒飯又是10塊錢,那么最侄訓傳的就是10塊錢
*/
public abstract float cost();
}
接著,創建兩個快餐類的子類,一個是炒飯類,這里我們命名為FriedRice,
package com.meimeixia.pattern.decorator;
/**
* 炒飯類(具體構件角色)
* @author liayun
* @create 2021-07-31 13:10
*/
public class FriedRice extends FastFood {
/*
* 在FriedRice類中,我們只需要給它提供一個無參的構造方法就可以了,但是我們得通過該無參構造給父類中的兩個成員變數進行賦值,
* 如果客戶選擇的是炒飯,而炒飯的價格又是固定的,比如10塊錢,那么代碼就應該向下面這樣寫,
*/
public FriedRice() {
super(10, "炒飯");
}
@Override
public float cost() {
return getPrice(); // 由于我們剛才已經定義好了炒飯的價格是10塊錢,所以此處我們直接呼叫
// 父類中的getPrice方法就能獲取到價格了
}
}
現在,如果我們想要點一份炒飯,那么是不是只需要創建FriedRice類的物件就可以了啦!而且這樣就會自動將炒飯的價格設定為10塊錢,描述那當然就是炒飯了啊!最終,我們去計算一份炒飯的餐費的話,那就是10塊錢了,這是非常合情合理的,
FriedRice類還要一個子類,就是炒面類,這里我們命名為FriedNoodles,
package com.meimeixia.pattern.decorator;
/**
* 炒面類(具體構件角色)
* @author liayun
* @create 2021-07-31 13:17
*/
public class FriedNoodles extends FastFood {
/*
* 在FriedNoodles類中,我們只需要給它提供一個無參的構造方法就可以了,但是我們得通過該無參構造給父類中的兩個成員變數進行賦值,
* 如果客戶選擇的是炒面,而炒面的價格又是固定的,比如12塊錢,那么代碼就應該像下面這樣寫,
*/
public FriedNoodles() {
super(12, "炒面");
}
@Override
public float cost() {
return getPrice(); // 由于我們剛才已經定義好了炒面的價格是12塊錢,所以此處我們直接呼叫
// 父類中的getPrice方法就能獲取到價格了
}
}
緊接著,創建最核心的類,即Garnish,它就是裝飾者類,對于該裝飾者類,大家一定要注意它設計的一個原則,就是它得去繼承FastFood類,雖說是去繼承,但是在這里我們就不重寫它里面的方法了,注意了,我們應該將該裝飾者類定義成抽象的,為什么呢?因為快餐具體要加哪種配料,我們是不明確的,不明確的話,那么總價就無法進行計算了,所以我們就需要把該裝飾者類定義成抽象類了,
你覺得該裝飾者類屬于裝飾者模式里面的哪種角色呢?很明顯,它屬于抽象裝飾角色,注意了,上面我們還沒說完Garnish類設計的原則呢,下面我們接著來說,
Garnish類除了要去繼承FastFood類之外,還得聚合FastFood類的物件,所以我們就得在Garnish類的成員位置宣告FastFood類的變數了,這是裝飾者模式的一個顯著特點,記住,定義完FastFood型別的成員變數之后,還得為其對應的getter和setter方法,
這樣,Garnish類的代碼就呼之欲出了,
package com.meimeixia.pattern.decorator;
/**
* 裝飾者類(抽象裝飾者角色)
* @author liayun
* @create 2021-07-31 13:27
*/
public abstract class Garnish extends FastFood {
// 宣告快餐類的變數
private FastFood fastFood;
public FastFood getFastFood() {
return fastFood;
}
public void setFastFood(FastFood fastFood) {
this.fastFood = fastFood;
}
/*
* 在Garnish類中,我們還得提供如下有參構造,價格與描述這兩屬性是直接呼叫父類中的方法來進行設定的,
* 至于FastFood型別的屬性,懂得都懂!!!
*
* 注意,后面的兩個引數所代表的意思,
* float price:配料(例如雞蛋)的價格,例如,一個炒雞蛋是1塊錢
* String desc:配料(例如雞蛋)的描述,例如,雞蛋的描述肯定就是雞蛋了
*/
public Garnish(FastFood fastFood, float price, String desc) {
super(price, desc);
this.fastFood = fastFood;
}
}
裝飾者類創建完畢之后,接下來,我們就得開始創建配料類了,第一個配料類是雞蛋類,即Egg,注意了,它得繼承Garnish裝飾者類,
繼承完了之后,我們得來思考一下,對于該雞蛋類來說,我們肯定是要提供構造方法的,那么是提供有參的構造方法還是無參的構造方法呢?很明顯是提供有參的構造方法,因為我們還得給父類中的快餐類成員變數進行賦值呢!你不可能只要配料而不要具體的快餐吧!比如說你就去快餐店只點兩個炒雞蛋,而不來一份炒飯,當然了,你非得干吃兩個炒雞蛋那也不是不可以,只是這種情況很少很少,
除此之外,在該雞蛋類中,我們還得重寫父類中的cost方法以便計算快餐加配料之后的總價,如何來重寫父類中的cost方法呢?很簡單,雞蛋的價格加上快餐的價格就計算出來了,
重寫完父類中的cost方法之后,注意了,我們還得重寫父類中的getDesc方法,重寫也很簡單,就是雞蛋的描述拼接上快餐的描述就行了,
這樣,第一個配料類(即Egg)的代碼就呼之欲出了,
package com.meimeixia.pattern.decorator;
/**
* 雞蛋類(具體的裝飾者角色)
* @author liayun
* @create 2021-07-31 13:40
*/
public class Egg extends Garnish {
public Egg(FastFood fastFood) {
super(fastFood, 1, "雞蛋");
}
@Override
public float cost() {
// 計算價格
return getPrice() + getFastFood().cost();
}
@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}
同理,第二個配料類(即Bacon)的代碼就不難寫出了,
package com.meimeixia.pattern.decorator;
/**
* 培根類(具體的裝飾者角色)
* @author liayun
* @create 2021-07-31 13:40
*/
public class Bacon extends Garnish {
public Bacon(FastFood fastFood) {
super(fastFood, 2, "培根");
}
@Override
public float cost() {
// 計算價格
return getPrice() + getFastFood().cost();
}
@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}
最后,我們就要撰寫一個客戶端類來測驗一下了,
package com.meimeixia.pattern.decorator;
/**
* @author liayun
* @create 2021-07-31 15:32
*/
public class Client {
public static void main(String[] args) {
// 點一份炒飯
FastFood food = new FriedRice();
// 列印炒飯的價格與描述
System.out.println(food.getDesc() + " " + food.cost() + "元");
}
}
此時,運行以上客戶端類,如下圖所示,可以看到列印的炒飯的價格確實是10塊錢,因為我們之前對炒飯的價格設定就是10塊錢,

如果我們此時想在上面的炒飯里面加上一個炒雞蛋,那么應該怎么去做呢?測驗代碼是不是應該像下面這樣啊!
package com.meimeixia.pattern.decorator;
/**
* @author liayun
* @create 2021-07-31 15:32
*/
public class Client {
public static void main(String[] args) {
// 點一份炒飯
FastFood food = new FriedRice();
// 列印炒飯的價格與描述
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("===================");
// 在上面的炒飯中加一個雞蛋
food = new Egg(food);
// 列印炒飯加上雞蛋之后的價格與描述
System.out.println(food.getDesc() + " " + food.cost() + "元");
}
}
再運行以上客戶端類,如下圖所示,可以看到炒飯加了一個雞蛋之后,總價確實變成了11塊錢,

現在我還能在以上雞蛋炒飯里面再加上一個炒雞蛋嗎?顯然是可以的啊!測驗代碼如下所示,
package com.meimeixia.pattern.decorator;
/**
* @author liayun
* @create 2021-07-31 15:32
*/
public class Client {
public static void main(String[] args) {
// 點一份炒飯
FastFood food = new FriedRice();
// 列印炒飯的價格與描述
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("===================");
// 在上面的炒飯中加一個雞蛋
food = new Egg(food);
// 列印炒飯加上雞蛋之后的價格與描述
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("===================");
// 再加一個雞蛋
food = new Egg(food);
System.out.println(food.getDesc() + " " + food.cost() + "元");
}
}
再運行以上客戶端類,如下圖所示,可以看到雞蛋炒飯加了一個炒雞蛋之后,總價確實變成了12塊錢,

那么我們還能再給以上雞蛋雞蛋炒飯加一根培根嗎?顯然是可以的啊!測驗代碼如下所示,
package com.meimeixia.pattern.decorator;
/**
* @author liayun
* @create 2021-07-31 15:32
*/
public class Client {
public static void main(String[] args) {
// 點一份炒飯
FastFood food = new FriedRice();
// 列印炒飯的價格與描述
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("===================");
// 在上面的炒飯中加一個雞蛋
food = new Egg(food);
// 列印炒飯加上雞蛋之后的價格與描述
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("===================");
// 再加一個雞蛋
food = new Egg(food);
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("===================");
// 再加一個培根
food = new Bacon(food);
System.out.println(food.getDesc() + " " + food.cost() + "元");
}
}
再運行以上客戶端類,如下圖所示,可以看到雞蛋雞蛋炒飯加了一根培根之后,總價確實變成了14塊錢,

當然,對于炒面,你也可以參照以上代碼再去進行測驗,只不過這里我就略過了,
最終,你會發現使用裝飾者模式設計出來的系統會特別特別靈活,如果此時我們想要再去新增一個配料的話,例如火腿腸,那么我們只需要再去定義一個火腿腸類,然后讓它去繼承裝飾者類就可以了,
裝飾者模式的好處以及使用場景
好處
裝飾者模式的好處,我總結出來了如下兩個,
-
裝飾者模式可以帶來比繼承更加靈活性的擴展功能(這是因為裝飾者模式本身就是在原有的基礎上進行了一個擴展),使用更加方便,可以通過組合不同的裝飾者物件來獲取具有不同行為狀態的多樣化的結果(比如說,你可以在炒飯的基礎上加雞蛋、加培根),裝飾者模式比繼承更具良好的擴展性,完美的遵循開閉原則,繼承是靜態的附加責任,裝飾者則是動態的附加責任
-
裝飾者類和被裝飾者類可以獨立發展,不會相互耦合,裝飾者模式是繼承的一個替代模式,裝飾者模式可以動態擴展一個實作類的功能,
上面這句話說的是啥意思啊?我用上面的快餐店案例來給大家詳細解釋一下,如果現在我們要再添加一種品類的快餐的話,例如炒河粉,那么是不是只需要再去定義一個炒河粉類,然后讓它直接去繼承FastFood類就可以了啊?而如果此時我們想要再去添加一個配料的話,例如火腿腸,那么是不是只需要再去定義一個火腿腸類,然后讓它去繼承裝飾者類就可以了啊?這樣,裝飾者類和被裝飾者類不就可以獨立發展了嘛!而且它們還不會相互耦合
使用場景
裝飾者模式的使用場景,我總結出來了如下三個,
-
當不能采用繼承的方式對系統進行擴充或者采用繼承不利于系統擴展和維護時,我們就可以使用裝飾者模式了,
不能采用繼承的情況主要有兩類:
- 第一類是系統中存在大量獨立的擴展(比如說配料或者快餐的品種),為支持每一種組合將產生大量的子類,使得子類數目呈爆炸性增長,而定義太多的子類會讓系統變得更加的復雜
- 第二類是因為類定義不能繼承(如final類)
-
在不影響其他物件的情況下,以動態、透明的方式給單個物件添加職責
-
當物件的功能要求可以動態地添加,也可以再動態地撤銷時,
哎,動態地撤銷又該怎么去理解呢?我用上面的快餐店案例再來給大家詳細解釋一下,快餐店里面的雞蛋賣完了之后,我們只需要動態地把Egg這個子類移除掉就可以了;又買了一些雞蛋之后,再將該子類添加上就行,這就是所謂的動態地添加和撤銷
裝飾者模式在JDK原始碼中的應用
接下來,我們來看下裝飾者模式在JDK原始碼里面是如何應用的?
IO流中的包裝類使用到了裝飾者模式,而哪些屬于包裝類呢?像BufferedInputStream、BufferedOutputStream、BufferedReader以及BufferedWriter等這些都屬于包裝類,也就是說這些類都用到了裝飾者模式,下面我們就以BufferedWriter舉例來說明一下,
我們不妨先看看如何使用BufferedWriter類吧!
public class Demo {
public static void main(String[] args) throws Exception{
// 創建BufferedWriter物件
// 創建FileWriter物件
FileWriter fw = new FileWriter("C:\\Users\\liayun\\Desktop\\a.txt");
BufferedWriter bw = new BufferedWriter(fw);
//寫資料
bw.write("Hello Buffered");
bw.close();
}
}
以上代碼很簡單,我們是要去創建BufferedWriter物件的,但是其構造方法里面需要一個Writer的子實作類物件,所以我們得提前創建一個FileWriter物件,并把它作為引數進行一個傳遞,然后,我們就能使用緩沖流里面的write方法進行資料的一個寫出操作了,最后就是釋放資源,
當然以上代碼我在這里就不給大家運行演示了,如果你要是有興趣的話,不妨私下自己去跑一跑,
簡單使用了一下BufferedWriter類之后,你會發現它使用起來感覺確實像是裝飾者模式,那么到底是不是呢?我們來看一下下面這張類圖,

可以看到,頂層父類是Writer,它是字符輸出流的頂層父類,并且它還有幾個子類,一個子類是InputStreamWriter(該類下面又有一個子類,即FileWriter),一個子類是BufferedWriter,
你注意看,BufferedWriter類同時又聚合了Writer類,也就是說BufferedWriter類不僅繼承自Writer類,并且還聚合了Writer類,這很明顯就是裝飾者模式,裝飾者模式的巧妙之處就在于這,
至此,通過以上類圖,我們就分析出了BufferedWriter類確實是用到了裝飾者模式,至于其他的幾個類,大家有興趣的話,可以自己去查看一下它們的源代碼,看一下它們是不是也用到了裝飾者模式,只是我在這里就不贅述了,
最后,我做一個小結,BufferedWriter使用裝飾者模式對Writer子實作類進行了增強,即添加了緩沖區,提高了寫資料的效率,
裝飾者模式和靜態代理的區別
接下來,我們來說說靜態代理和裝飾者模式的一個區別,因為它倆很像很像,不熟悉它倆之間區別的人很容易把它們搞混到一塊去,
相同點
裝飾者模式和靜態代理的相同點:
-
都要實作與目標類相同的業務介面,
這是說的啥意思呢?我們不妨往上看一下上面的快餐點案例的類圖,可以看到都得去繼承(或者實作)FastFood抽象類,是不是啊?
-
在兩個類中都要宣告目標物件,也就是說,都要把你繼承的那個類的子類物件給聚合進來
-
都可以在不修改目標類的前提下增強目標方法
以上幾點看下來,靜態代理和裝飾者模式是不是特像啊!
不同點
裝飾者模式和靜態代理的不同點:
-
目的不同,裝飾者模式是為了增強目標物件,而靜態代理是為了保護和隱藏目標物件,
這話什么意思呢?回顧一下使用裝飾者模式實作的快餐店案例的代碼,在裝飾者類(即Garnish)中,是不是宣告了一個FastFood型別的成員變數啊!你注意了,在這兒我們并沒有對該成員變數進行賦值,那么它應該由誰來賦值呢?誰使用,誰就對該成員變數進行賦值;而如果是靜態代理的話,那么在這一塊就是直接創建一個FastFood物件,并將其賦值給該成員變數了,這就相當于是保護和隱藏了目標物件
-
獲取目標物件構建的地方不同,裝飾者模式是由外界傳遞進來的,可以通過構造方法傳遞,當然我們也可以通過setter方法進行一個傳遞;而靜態代理是在代理類內部創建的,也就是說如果代理類是Garnish這個類的話,那么在成員變數處就直接創建了FastFood物件,這樣做的目的就是為了隱藏目標物件
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/294934.html
標籤:java
