23種設計模式——結構型設計模式(7種)
目錄
☞ 23 種設計模式——結構型設計模式(7種)
☞ 23 種設計模式——行為型設計模式(11種)
3. 結構型設計模式
結構型模式描述如何將類或對象按某種布局組成更大的結構。它分為類結構型模式和對象結構型模式,前者採用繼承機制來組織接口和類,後者採用組合或聚合組合對象。
由於組合關係或聚合關係比較繼承關係耦合度低,滿足「合成複合原則」,所以對象結構型模式比類結構型模式具有更大的靈活性。
結構型模式分為以下 7 種:
1)代理(Proxy)模式:為某對象提供一種代理以控制對象的訪問。即客戶端通過代理簡介地訪問該對象,從而限制、增強或修改該對象的一些特徵。
2)適配器(Adapter)模式:將一個類的接口轉換成希望的另一個接口,使得原本由於接口不兼容而不能一起工作的那些類能一起工作。
3)橋接(Bridge)模式:將抽象與實現分離,使它們可以獨立變化。它是用組合關係代替繼承關係來實現的,從而降低了抽象和實現這兩個可變維度的耦合度。
4)裝飾(Decorator)模式:動態地給對象增加一些職責,即增加其額外的功能。
5)外觀(Facade)模式:為多個複雜的子系統提供一個一致的接口,使這些子系統更加容易被訪問。
6)享元(Flyweight)模式:運用共享技術來有效地支持大量細粒度對象的復用。
3.1 代理(Proxy)模式
在有些情況下,一個客戶不能或者不想直接訪問另一個對象,這時需要找一個中介幫忙完成某項任務,這個中介就是代理對象。例如,購買火車票不一定要去火車站買,可以通過 12306 網站或者去火車票代售點購買。又如找女朋友、找保姆、找工作都可以通過中介完成。
在軟件設計中,使用代理模式的例子很多,如,要訪問原型對象比較大(如視頻或者大圖像等),其下載要花很多時間。還有因為安全需要屏蔽客戶端直接訪問真實對象,如某單位的內部數據庫等。
3.1.1 代理模式的定義與特點
代理模式的定義:由於某些原因需要給某對象提供一個代理以控制對該對象的訪問。這時,訪問對象不合適或者不能直接引用目標對象,代理對象作為訪問對象和目標對象之間的中介。
代理模式的主要優點:
- 代理模式在客戶端與目標對象之間起到一個中介作用和保護目標對象的作用;
- 代理模式可以擴展目標對象的功能;
- 代理模式能將客戶端與目標對象分離,在一定程度上降低了系統的耦合度。
其主要缺點是:
- 在客戶端和目標對象之間增加一個代理對象,會造成請求處理速度變慢;
- 增加了系統的複雜度。
3.1.2 代理模式的結構與實現
代理模式的結構比較簡單,只要是通過定義一個集成抽象主題的代理來包含真實主題,從而實現對真實主題訪問,下面來分析基本結構和實現方法。
(1)模式的結構
代理模式的主要角色如下:
1)抽象主題類:通過接口或抽象類聲明真實主題或代理對象實現的業務方法。
2)真實主題類:實現了抽象主題中的具體業務,是代理對象所代表的真實對象,是最終要 引用的對象。
3)代理類:提供了與真實主題相同的接口,其內部含有對真實主題的引用,它可以訪問、控制或擴展真實主題的功能。
其結構圖如圖3-1所以:
圖3-1 代理模式的結構圖
(2)模式的實現
代理模式的實現代碼如下:
1 // 抽象主題 2 interface Subject { 3 void request(); 4 }
1 // 真實主題 2 class RealSubject implements Subject { 3 public void request() { 4 System.out.println("訪問真實主題方法..."); 5 } 6 }
1 // 代理 2 class Proxy implements Subject { 3 private RealSubject realSubject; 4 public void request() { 5 if (realSubject == null) { 6 realSubject = new RealSubject(); 7 } 8 preRequest(); 9 realSubject.request(); 10 postRequest(); 11 } 12 public void preRequest() { 13 System.out.println("訪問真實主題之前的預處理。"); 14 } 15 public void postRequest() { 16 System.out.println("訪問真實主題之後的後續處理。"); 17 } 18 }
1 public class ProxyTest { 2 public static void main(String[] args) { 3 Proxy proxy = new Proxy(); 4 proxy.Request(); 5 } 6 }
程序運行的結果如下:
訪問真實主題之前的預處理。
訪問真實主題方法...
訪問真實主題之後的後續處理。
3.1.3 代理模式的應用場景
前面分析了代理模式的結構與特點,現在分析以下的應用 場景 。
● 遠程代理,這種方法通常是為了隱藏目標對象目標存在於不同地址空間的事實,方便客戶端訪問。例如,用戶申請某些網盤空間時,會在用戶的文件系統中建立一個虛擬的硬盤,用戶訪問虛擬硬盤時實際訪問的是網盤空間。
● 虛擬代理,這種方式通常用於要創建的目標對象開銷很大。例如,下載一個很大的圖片需要很長時間,因某種計算比較複雜而短時間無法完成,這時可以先用小比例的虛擬代理替換真實的對象,消除用戶對服務器慢的感覺。
● 安全代理,這種方式通常用於控制不同種類客戶對真實對象的訪問權限。
● 智能指引,主要用於調用目標對象時,代理附加一些額外的處理功能。例如,增加計算真實對象的引用次數的功能,這樣當該對象沒有被引用時,就可以自動釋放它。
● 延遲加載,指為了提高系統的性能,延遲對目標的加載。例如,Hibernate 中就存在屬性的延遲加載和關聯表的延時加載。
3.1.4 代理模式的擴展
在前面介紹的代理模式中,代理類中包含了對真實主題引用,這種方式存在兩個缺點。
1)真實主題與代理主題一一對應,增加真實主題也要增加代理。
2)設計代理以前真實主題必須事先存在,不太靈活。採用動態代理模式可以解決以上問題,如 SpringAOP,其結構圖如圖3-2所示。
3.2 適配器(Adapter)模式
在現實生活中,經常出現兩個對象因接口不兼容而不能再一起工作的實例,這時需要第三者進行適配。例如,講中文的人同講英文的人對話時需要一個翻譯,用直流電的筆記本電腦接交流電源時需要一個電源適配器。
在軟件設計中也可能出現:需要開發的具有某種業務功能的組件在現有的組件庫中已經存在,但它們與當前系統的接口規範不兼容,如果重寫開發這些組件成本又很高,這時用是適配器模式能很好地解決這些問題。
3.2.1 模式的定義與特點
適配器模式的定義如下:將一個類的接口轉換成客戶希望的另一個接口,使得原本由於接口不兼容而不能一起功能的那些類能一起工作。適配器模式分為類結構型模式和對象結構型模式兩種,前者之間的耦合度比後者高,其要求程序員了解現有組件庫中的相關的內部結構,所以應對相對較少些。
該模式的主要優點如下:
● 客戶端通過適配器可以透明地調用目標接口。
● 復用了現存的類,程序員不需要修改原有代碼而重用現有的適配者類。
● 將目標和適配這類解耦,解決了目標類和適配類接口不一致問題。
其缺點是:
對類適配器來說,更換適配器的實現過程比較複雜。
3.2.2 模式的結構與實現
類適配器模式可採用充多充繼承方式實現,如 C++ 可以定義一個適配器類來同時繼承房錢系統的業務接口和現有組件庫中已經存在的組件接口;Java 不支持多繼承,但可以定義一個適配器來來實現當前系統的業務接口,同時又繼承現有組件庫中已經存在的組件。
對象適配器模式可採用將現有組件庫中已經實現的組件引入適配器類,該類同時實現當前系統的業務接口。現在來介紹他們的基本結構。
(1)模式的結構
適配器模式包含以下主要角色。
1)目標接口:當前系統業務所期待的接口,它可以是抽象類或接口。
2)適配類:它是被訪問和適配的現存組件庫中的組件接口。
3)適配器類:它是一個轉換器,通過繼承或引用適配者的對象,把適配器接口轉換成目標接口,讓客戶目標接口的可是訪問適配者。
類適配器模式的結構圖,如圖3-2所示:
3-2 類適配器模式的結構圖
對象適配器模式的結構圖,如圖3-3所示:
圖3-3對象適配器模式的結構圖
(2)模式的實現
1 // 目標接口 2 interface Target { 3 public void request(); 4 }
① 類適配器模式的代碼如下:
1 // 適配者接口 2 class Adaptee { 3 public void specificRequest() { 4 System.out.println("適配者中的業務代碼被調用!"); 5 } 6 } 7 8 // 類適配器類 9 class ClassAdapter extends Adaptee implements Target { 10 public void request() { 11 specificRequest(); 12 } 13 }
1 //客戶端代碼 2 public class ClassAdapterTest { 3 public static void main(String[] args) { 4 System.out.println("類適配器模式測試:"); 5 Target target = new ClassAdapter(); 6 target.request(); 7 } 8 }
程序運行結果如下:
類適配器模式測試:
適配者中的業務代碼被調用!
② 對象適配器模式的代碼如下:
1 // 對象適配器類 2 class ObjectAdapter implements Target { 3 private Adaptee adaptee; 4 public ObjectAdapter(Adaptee adaptee) { 5 this.adaptee = adaptee; 6 } 7 public void request() { 8 adaptee.specificRequest(); 9 } 10 }
1 // 客戶端代碼 2 public class ObjectAdapterTest { 3 public static void main(String[] args) { 4 System.out.println("對象適配器模式測試:"); 5 Adaptee adaptee = new Adaptee(); 6 Target target = new ObjectAdapter(adaptee); 7 target.request(); 8 } 9 }
程序運行結果如下:
對象適配器模式測試:
適配者中的業務代碼被調用!
說明:對象適配器模式中的「目標接口」和「適配者類」的代碼同類適配器模式一樣,只要修改適配器類和客戶端的代碼即可。
3.2.3 模式的應用實例
【例】用適配器模式(Adapter)模式新能源汽車的發動機。
分析:新能源汽車的發動機有電能發動機和光能發動機等,各種發動機的驅動,例如,電能發動機的驅動方法 electricDrive() 是用電能驅動,而光能發動機的驅動方法 opticalDrice() 用光能驅動,它們是適配器模式中被訪問的適配器。
客戶端希望用統一的發動機驅動方法 drive() 訪問這兩種發動機,所以必須定義一個統一的目標接口 Motor,然後再定義電能適配器(Electric Adapter)和光能適配器(Optical Adapter)去適配這兩種發動機。結構圖如圖3-4所示。
圖3-4 發動機適配器的結構圖
程序代碼如下:
1 // 目標:發動機 2 interface Motor { 3 public void drive(); 4 } 5 6 // 適配者1:電能發動機 7 class ElectricMotor { 8 public void electricDrive() 9 { 10 System.out.println("電能發動機驅動汽車!"); 11 } 12 } 13 14 // 適配者2:光能發動機 15 class OpticalMotor { 16 public void opticalDrive() { 17 System.out.println("光能發動機驅動汽車!"); 18 } 19 } 20 21 // 電能適配器 22 class ElectricAdapter implements Motor { 23 private ElectricMotor emotor; 24 public ElectricAdapter() { 25 emotor = new ElectricMotor(); 26 } 27 public void drive() { 28 emotor.electricDrive(); 29 } 30 } 31 32 // 光能適配器 33 class OpticalAdapter implements Motor { 34 private OpticalMotor omotor; 35 public OpticalAdapter() { 36 omotor = new OpticalMotor(); 37 } 38 public void drive() { 39 omotor.opticalDrive(); 40 } 41 }1 public class MotorAdapterTest { 2 public static void main(String[] args) { 3 Motor mEle = new ElectricAdapter(); 4 mEle.drive(); 5 6 Motor mOpt = new OpticalAdapter(); 7 mOpt.drive(); 8 } 9 }程序運行結果:
電能發動機驅動汽車! 光能發動機驅動汽車!
3.2.4 模式的應用場景
適配器模式(Adapter)通常適用於以下場景:
- 以前開發的系統存在滿足新系統功能需求的類,但其接口同新系統的接口不一致。
- 使用第三方提供的組件,但組件接口定義和自己要求的接口定義不同。
3.4.5 模式的擴展
適配器模式可擴展為雙向適配器模式,雙向適配器類既可以把適配者接口轉換成目標接口,也可以吧目標接口轉換成適配者接口,其結構圖如圖3-5所示。
圖3-5 雙向適配器模式的結構圖
程序代碼如下:
1 //目標接口 2 interface TwoWayTarget { 3 public void request(); 4 } 5 6 //目標實現 7 class TargetRealize implements TwoWayTarget { 8 public void request() { 9 System.out.println("目標代碼被調用!"); 10 } 11 }
1 // 適配者接口 2 interface TwoWayAdaptee { 3 public void specificRequest(); 4 } 5 6 // 適配者實現 7 class AdapteeRealize implements TwoWayAdaptee { 8 public void specificRequest() { 9 System.out.println("適配者代碼被調用!"); 10 } 11 }
1 // 雙向適配器 2 class TwoWayAdapter implements TwoWayTarget, TwoWayAdaptee { 3 private TwoWayTarget target; 4 public TwoWayAdapter(TwoWayTarget target) { 5 this.target = target; 6 } 7 public void specificRequest() { 8 target.request(); 9 } 10 11 private TwoWayAdaptee adaptee; 12 public TwoWayAdapter(TwoWayAdaptee adaptee) { 13 this.adaptee = adaptee; 14 } 15 public void request() { 16 adaptee.specificRequest(); 17 } 18 }
1 // 客戶端代碼 2 public class TwoWayAdapterTest { 3 public static void main(String[] args) { 4 5 System.out.println("目標通過雙向適配器訪問適配者:"); 6 TwoWayAdaptee adaptee = new AdapteeRealize(); 7 TwoWayTarget target = new TwoWayAdapter(adaptee); 8 target.request(); 9 10 System.out.println("-------------------"); 11 12 System.out.println("適配者通過雙向適配器訪問目標:"); 13 target = new TargetRealize(); 14 adaptee = new TwoWayAdapter(target); 15 adaptee.specificRequest(); 16 } 17 }
程序運行結果如下:
目標通過雙向適配器訪問適配者: 適配者代碼被調用! ------------------- 適配者通過雙向適配器訪問目標: 目標代碼被調用!
3.3 橋接(Bridge)模式
在現實生活中,默寫具有兩個或多個維度的變化,如圖像即可按形狀分,又可按顏色分。如果設計類似於 Photoshop 這樣的軟件,如何去畫不同形狀和不同顏色的圖像呢?如果用繼承方式,m 種形狀和 n 種顏色的圖形就有 m×n 種,不但對應的子類很多,而且擴展困難。
當然,這樣的例子還有很多,如不同顏色和字體的文字、不同品牌和功率的汽車、不同性別和職業的男女。。如果用橋接模式就能很好地解決這些問題。
3.3.1 橋接模式的定義和特點
橋接模式的定義如下:將抽象與實現分離,使它們可以獨立變化。它是用組合關係代替繼承關係來實現,從而降低了抽象和實現這兩個可變維度的耦合度。
橋接模式的優點:
● 由於抽象與實現分離,所以擴展能力強。
● 其實現細節對客戶透明。
缺點是:由於聚合關係建立在抽象層,要求開發者針對抽象畫進行設計與編程,這增加了系統的理解與設計難度。
3.3.2 橋接模式的結構與實現
可以將抽象化部分與實現化部分分來,取消二者的繼承關係,改用組合關係。
(1)模式的結構
橋接模式包含以下主要角色:
1)抽象化角色:定義抽象類,並包含一個對實現化對象的引用。
2)擴展抽象化角色:是抽象化角色的子類,實現父類中的業務方法,並通過組合關係調用實現化角色中的業務方法。
3)實現化角色:定義是實現化角色的接口,供擴展抽象化角色調用。
4)具體實現化角色:給出實現化角色接口的具體實現。
其結構圖如圖3-6所示。
圖3-6 橋接模式的結構圖
3.3.3 橋接模式的應用實例
【例】用橋接(Bridge)模式模擬女士皮包的選購。
分析:女士皮包有很多種,可以按用途分,按皮質分、按品牌分、按顏色分、按大小分、存在讀個維度的變化,所以採用橋接模式來實現女士皮包的選購比較合適。
本實例按用途分為:錢包(Wallet)和挎包(HandBag),按顏色分為:黃色(Yellow)和紅色(Red)。可以按兩個維度定義為顏色類和包類。
顏色類(Color)是一個維度,定義為實現化角色,它有兩個具體實現化角色:黃色和紅色,通過 getColor() 方法可以選擇顏色。
堡壘(Bag)是另一個維度,定義抽象化角色,它有兩個擴展抽象畫角色:錢包和挎包,它包含了顏色類對象,通過 getName() 方法可以選擇相關顏色的錢包和挎包。
圖 3-7 女士皮包選的結構圖
程序代碼如下:
1 // 實現化角色:顏色 2 interface Color { 3 String getColor(); 4 } 5 6 // 具體實現化角色:黃色 7 class Yellow implements Color { 8 public String getColor() { 9 return "yellow"; 10 } 11 } 12 13 // 具體實現化角色:紅色 14 class Red implements Color { 15 public String getColor() { 16 return "red"; 17 } 18 }1 // 抽象化角色:包 2 abstract class Bag { 3 protected Color color; 4 public void setColor(Color color) { 5 this.color = color; 6 } 7 public abstract String getName(); 8 } 9 10 // 擴展抽象化角色:挎包 11 class HandBag extends Bag { 12 public String getName() { 13 return color.getColor() + "-HandBag"; 14 } 15 } 16 17 // 擴展抽象化角色:錢包 18 class Wallet extends Bag { 19 public String getName() { 20 return color.getColor() + "-Wallet"; 21 } 22 }1 public class BridgeTest { 2 public static void main(String[] args) { 3 Color yellow = new Yellow(); 4 Bag bag = new Wallet(); 5 bag.setColor(yellow); 6 System.out.println(bag.getName()); 7 } 8 }程序運行結果:
yellow-Wallet
3.3.4 橋接模式的應用場景
橋接模式通常適用於以下場景:
- 當一個類存在兩個獨立變化的維度,其這兩個維度都需要進行擴展時。
- 當一個系統不希望使用繼承或因為多層次繼承導致系統類的個數急劇增加時。
- 當一個系統需要在結構的抽象畫角色和具體化角色之間增加更多的靈活性時。
3.3.5 橋接模式的擴展
在軟件開發中,有時橋接模式可以適配器模式聯合使用。當橋接模式的實現化角色的接口與現有類的接口不一致時,可以二者中間定義一個適配器將二者連接起來,其具體結構圖如圖3-8所示。
圖3-8 橋接模式與適配器模式聯用的結構圖
3.4 裝飾(Decorator)模式
在現實生活中,常常需要對心有產品增加新的功能或美化其外觀,如房子裝修、相片加相框等。在軟件開發過程中,有時想用一些現存的組件。這些組件可能只是完成了一些核心功能。但在不改變其結構的情況下,可以動態地擴展其功能。所有這些都可以釆用裝飾模式來實現。
3.4.1 裝飾模式的定義與特點
裝飾模式的定義:指在不改變現有對象結構的情況下,動態地給該對象增加一些職責(即增加其額外功能)的模式,它屬於對象結構型模式。
裝飾模式的主要優點有:
● 採用裝飾模式擴展比採用繼承方法更加靈活。
● 可以設計出多個不同的具體裝飾類,創造出多個不同行為的組合。
其主要缺點是:裝飾模式增加了許多子類,如果過度使用會使程序變得很複雜。
(1)模式的結構
裝飾模式主要包含以下角色。
1)抽象構件角色:定義一個抽象接口以規範準備接收附加責任的對象。
2)具體構件角色:實現抽象構件,通過裝飾角色為其添加一些職責。
3)抽象裝飾角色:繼承抽象構件,並包含具體構件的實例,可以通過其子類擴展具體構件的功能。
4)具體裝飾角色:實現抽象裝飾的相關方法,並給具體構件對象添加附加的責任。
裝飾模式的具體結構圖如圖3-9所示:
圖3-9 裝飾模式結構圖
(2)模式的實現
裝飾模式的實現代碼如下:
1 public class DecoratorPattern { 2 public static void main(String[] args) { 3 Component p = new ConcreteComponent(); 4 p.operation(); 5 System.out.println("---------------------------------"); 6 Component d = new ConcreteDecorator(p); 7 d.operation(); 8 } 9 } 10 11 // 抽象構件角色 12 interface Component { 13 public void operation(); 14 } 15 16 // 具體構件角色 17 class ConcreteComponent implements Component { 18 public ConcreteComponent() { 19 System.out.println("創建具體構件角色"); 20 } 21 public void operation() { 22 System.out.println("調用具體構件角色的方法operation()"); 23 } 24 } 25 26 // 抽象裝飾角色 27 class Decorator implements Component { 28 private Component component; 29 public Decorator(Component component) { 30 this.component = component; 31 } 32 public void operation() { 33 component.operation(); 34 } 35 } 36 // 具體裝飾角色 37 class ConcreteDecorator extends Decorator { 38 public ConcreteDecorator(Component component) { 39 super(component); 40 } 41 public void operation() { 42 super.operation(); 43 addedFunction(); 44 } 45 public void addedFunction() { 46 System.out.println("為具體構件角色增加額外的功能addedFunction()"); 47 } 48 }
程序運行結果如下:
創建具體構件角色 調用具體構件角色的方法operation() --------------------------------- 調用具體構件角色的方法operation() 為具體構件角色增加額外的功能addedFunction()
3.4.2 裝飾模式的應用實例
【例】用裝飾模式實現遊戲角色「莫莉卡·安斯蘭」的變身。
分析:在《惡魔戰士》中,遊戲角色「莫莉卡·安斯蘭」的原身是一個可愛少女,但當她變身時,會變成頭頂及背部延伸出蝙蝠狀飛翼的女妖,當然她還可以變為穿着漂亮外衣的少女。這些都可用裝飾模式來實現,在本實例中的「莫莉卡」原身有 setImage(String t) 方法決定其顯示方式,而其 變身「蝙蝠狀女妖」和「着裝少女」可以用 setChanger() 方法來改變其外觀,原身與變身後的效果用 display() 方法來顯示。
圖 3-10 所示其結構圖:
圖 3-10 遊戲角色 「莫莉卡·安斯蘭」 的結構圖
程序代碼如下:
1 //抽象構件角色:莫莉卡 2 interface Morrigan { 3 public void display(); 4 } 5 6 //具體構件角色:原身 7 class original extends JFrame implements Morrigan { 8 private static final long serialVersionUID = 1L; 9 private String t="Morrigan0.jpg"; 10 11 public original() { 12 super("《惡魔戰士》中的莫莉卡·安斯蘭"); 13 } 14 15 public void setImage(String t) { 16 this.t = t; 17 } 18 19 public void display() { 20 this.setLayout(new FlowLayout()); 21 JLabel l1=new JLabel(new ImageIcon("src/decorator/"+t)); 22 this.add(l1); 23 this.pack(); 24 this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 25 this.setVisible(true); 26 } 27 } 28 29 // 抽象裝飾角色:變形 30 class Changer implements Morrigan { 31 Morrigan m; 32 33 public Changer(Morrigan m) { 34 this.m = m; 35 } 36 37 public void display() { 38 m.display(); 39 } 40 } 41 42 // 具體裝飾角色:女妖 43 class Succubus extends Changer { 44 public Succubus(Morrigan m) { 45 super(m); 46 } 47 48 public void display() { 49 setChanger(); 50 super.display(); 51 } 52 53 public void setChanger() { 54 ((original) super.m).setImage("Morrigan1.jpg"); 55 } 56 } 57 58 // 具體裝飾角色:少女 59 class Girl extends Changer { 60 public Girl(Morrigan m) { 61 super(m); 62 } 63 64 public void display() { 65 setChanger(); 66 super.display(); 67 } 68 69 public void setChanger() { 70 ((original) super.m).setImage("Morrigan2.jpg"); 71 } 72 }1 public class MorriganAensland { 2 public static void main(String[] args) { 3 Morrigan m0=new original(); 4 m0.display(); 5 6 Morrigan m1=new Succubus(m0); 7 m1.display(); 8 9 Morrigan m2=new Girl(m0); 10 m2.display(); 11 } 12 }
程序運行結果如下:
圖 3-11 遊戲角色「莫莉卡·安斯蘭」的變身
3.4.3 裝飾模式的應用場景
前面講解了關於裝飾模式的結構與特點,下面介紹其適用的應用場景,裝飾模式通常在以下幾種情況使用。
- 當需要給一個現有類添加附加職責,而又不能採用生成子類的方法進行擴充時。例如,該類被隱藏或者該類是終極類或者採用繼承方式會產生大量的子類。
- 當需要通過對現有的一組基本功能進行排列組合而產生非常多的功能時,採用繼承關係很難實現,而採用裝飾模式卻很好實現。
- 當對象的功能要求可以動態地添加,也可以再動態地撤銷時。
裝飾模式在 Java 語言中的最著名的應用莫過於 Java I/O 標準庫的設計了。
例如,InputStream 的子類 FilterInputStream,OutputStream 的子類 FilterOutputStream,Reader 的子類 BufferedReader 以及 FilterReader,還有 Writer 的子類 BufferedWriter、FilterWriter 以及 PrintWriter 等,它們都是抽象裝飾類。
下面代碼是為 FileReader 增加緩衝區而採用的裝飾類 BufferedReader 的例子:
1 BufferedReader in = new BufferedReader(new FileReader("filename.txt")); 2 String str = in.readLine();
3.4.4 裝飾模式的擴展
裝飾模式所包含的 4 個角色不是任何時候都要存在的,在有些應用環境下模式是可以簡化的,如以下兩種情況。
(1) 如果只有一個具體構件而沒有抽象構件時,可以讓抽象裝飾繼承具體構件,其結構圖如圖 4 所示。
圖 3-12 只有一個具體構件的裝飾模式
(2)如果只有一個具體裝飾時,可以將抽象裝飾和具體裝飾合併,其結構圖如圖 3-13 所示:
圖 3-13 只有一個具體裝飾的裝飾模式
3.5 外觀(Facade)模式
在現實生活中,常常存在辦事較複雜的例子,如辦房產證或註冊一家公司,有時要同多個部門聯繫,這時要是有一個綜合部門能解決一切手續問題就好了。
軟件設計也是這樣,當一個系統的功能越來越強,子系統會越來越多,客戶對系統的訪問也變得越來越複雜。這時如果系統內部發生改變,客戶端也要跟着改變,這違背了「開閉原則」,也違背了「迪米特法則」,所以有必要為多個子系統提供一個統一的接口,從而降低系統的耦合度,這就是外觀模式的目標。
圖 3-14 給出了客戶去當地房產局辦理房產證過戶要遇到的相關部門。
圖 3-15 辦理房產證過戶的相關部門
3.5.1 外觀模式的定義與特點
外觀(Facade)模式的定義:是一種通過為多個複雜的子系統提供一個一致的接口,而使這些子系統更加容易被訪問的模式。該模式對外有一個統一接口,外部應用程序不用關心內部子系統的具體的細節,這樣會大大降低應用程序的複雜度,提高了程序的可維護性。
外觀(Facade)模式是 「迪米特法則」 的典型應用,它有以下主要優點。
1)降低了子系統與客戶端之間的耦合度,使得子系統的變化不會影響調用它的客戶類。
2)對客戶屏蔽了子系統組件,減少了客戶處理的對象數目,並使得子系統使用起來更加容易。
3)降低了大型軟件系統中的編譯依賴性,簡化了系統在不同平台之間的移植過程,因為編譯一個子系統不會影響其他的子系統,也不會影響外觀對象。
外觀(Facade)模式的主要缺點如下。
1)不能很好地限制客戶使用子系統類。
2)增加新的子系統可能需要修改外觀類或客戶端的源代碼,違背了「開閉原則」。
3.5.2 外觀模式的結構與實現
外觀(Facade)模式的結構比較簡單,主要是定義了一個高層接口。它包含了對各個子系統的引用,客戶端可以通過它訪問各個子系統的功能。現在來分析其基本結構和實現方法。
(1)模式的結構
外觀(Facade)模式包含以下主要角色。
1)外觀(Facade)角色:為多個子系統對外提供一個共同的接口。
2)子系統(Sub System)角色:實現系統的部分功能,客戶可以通過外觀角色訪問它。
3)客戶(Client)角色:通過一個外觀角色訪問各個子系統的功能。
其結構圖如圖 3-16 所示。
圖 3-16 外觀模式的結構圖
(2)模式的實現
外觀模式的實現代碼如下:
1 // 外觀角色 2 class Facade { 3 private SubSystem01 obj1 = new SubSystem01(); 4 private SubSystem02 obj2 = new SubSystem02(); 5 private SubSystem03 obj3 = new SubSystem03(); 6 public void method() { 7 obj1.method1(); 8 obj2.method2(); 9 obj3.method3(); 10 } 11 } 12 13 // 子系統角色 14 class SubSystem01 { 15 public void method1() { 16 System.out.println("子系統01的method1()被調用!"); 17 } 18 } 19 20 //子系統角色 21 class SubSystem02 { 22 public void method2() { 23 System.out.println("子系統02的method2()被調用!"); 24 } 25 } 26 27 // 子系統角色 28 class SubSystem03 { 29 public void method3() { 30 System.out.println("子系統03的method3()被調用!"); 31 } 32 } 33 34 public class FacadePattern { 35 public static void main(String[] args) { 36 Facade f = new Facade(); 37 f.method(); 38 } 39 }
程序運行結果如下:
子系統01的method1()被調用!
子系統02的method2()被調用!
子系統03的method3()被調用!
3.5.3 外觀模式的應用實例
【例】用「外觀模式」設計一個婺源特產的選購界面。
分析:本實例的外觀角色 WySpecialty 是 JPanel 的子類,它擁有 8 個子系統角色 Specialty1~Specialty8,它們是圖標類(ImageIcon)的子類對象,用來保存該婺源特產的圖標。
外觀類(WySpecialty)用 JTree 組件來管理婺源特產的名稱,並定義一個事件處理方法 valueClianged(TreeSelectionEvent e),當用戶從樹中選擇特產時,該特產的圖標對象保存在標籤(JLabd)對象中。
客戶窗體對象用分割面板來實現,左邊放外觀角色的目錄樹,右邊放顯示所選特產圖像的標籤。
其結構圖如圖 3–17 所示。
圖 3-17 婺源特產管理界面的結構圖
程序代碼如下:
1 class WySpecialty extends JPanel implements TreeSelectionListener { 2 private static final long serialVersionUID=1L; 3 final JTree tree; 4 JLabel label; 5 private Specialty1 s1=new Specialty1(); 6 private Specialty2 s2=new Specialty2(); 7 private Specialty3 s3=new Specialty3(); 8 private Specialty4 s4=new Specialty4(); 9 private Specialty5 s5=new Specialty5(); 10 private Specialty6 s6=new Specialty6(); 11 private Specialty7 s7=new Specialty7(); 12 private Specialty8 s8=new Specialty8(); 13 14 WySpecialty() { 15 DefaultMutableTreeNode top = new DefaultMutableTreeNode("婺源特產"); 16 17 DefaultMutableTreeNode node1 = null, node2 = null, tempNode = null; 18 19 // 四大特產 20 node1 = new DefaultMutableTreeNode("婺源四大特產(紅、綠、黑、白)"); 21 22 tempNode = new DefaultMutableTreeNode("婺源荷包紅鯉魚"); 23 node1.add(tempNode); 24 25 tempNode=new DefaultMutableTreeNode("婺源綠茶"); 26 node1.add(tempNode); 27 28 tempNode=new DefaultMutableTreeNode("婺源龍尾硯"); 29 node1.add(tempNode); 30 31 tempNode=new DefaultMutableTreeNode("婺源江灣雪梨"); 32 node1.add(tempNode); 33 34 top.add(node1); 35 36 // 其他特產 37 node2 = new DefaultMutableTreeNode("婺源其它土特產"); 38 39 tempNode = new DefaultMutableTreeNode("婺源酒糟魚"); 40 node2.add(tempNode); 41 42 tempNode = new DefaultMutableTreeNode("婺源糟米子糕"); 43 node2.add(tempNode); 44 45 tempNode = new DefaultMutableTreeNode("婺源清明果"); 46 node2.add(tempNode); 47 48 tempNode = new DefaultMutableTreeNode("婺源油煎燈"); 49 node2.add(tempNode); 50 51 top.add(node2); 52 53 // 樹目錄 54 tree = new JTree(top); 55 tree.addTreeSelectionListener(this); 56 label = new JLabel(); 57 } 58 59 public void valueChanged(TreeSelectionEvent e) { 60 if(e.getSource() == tree) { 61 DefaultMutableTreeNode node=(DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); 62 if(node==null) return; 63 64 if(node.isLeaf()) { 65 Object object = node.getUserObject(); 66 String sele = object.toString(); 67 label.setText(sele); 68 label.setHorizontalTextPosition(JLabel.CENTER); 69 label.setVerticalTextPosition(JLabel.BOTTOM); 70 sele = sele.substring(2,4); 71 72 if(sele.equalsIgnoreCase("荷包")) label.setIcon(s1); 73 else if(sele.equalsIgnoreCase("綠茶")) label.setIcon(s2); 74 else if(sele.equalsIgnoreCase("龍尾")) label.setIcon(s3); 75 else if(sele.equalsIgnoreCase("江灣")) label.setIcon(s4); 76 else if(sele.equalsIgnoreCase("酒糟")) label.setIcon(s5); 77 else if(sele.equalsIgnoreCase("糟米")) label.setIcon(s6); 78 else if(sele.equalsIgnoreCase("清明")) label.setIcon(s7); 79 else if(sele.equalsIgnoreCase("油煎")) label.setIcon(s8); 80 81 label.setHorizontalAlignment(JLabel.CENTER); 82 } 83 } 84 } 85 } 86 87 class Specialty1 extends ImageIcon { 88 private static final long serialVersionUID=1L; 89 Specialty1() { 90 super("src/facade/WyImage/Specialty11.jpg"); 91 } 92 } 93 94 class Specialty2 extends ImageIcon { 95 private static final long serialVersionUID=1L; 96 Specialty2() { 97 super("src/facade/WyImage/Specialty12.jpg"); 98 } 99 } 100 101 class Specialty3 extends ImageIcon { 102 private static final long serialVersionUID=1L; 103 Specialty3() { 104 super("src/facade/WyImage/Specialty13.jpg"); 105 } 106 } 107 108 class Specialty4 extends ImageIcon { 109 private static final long serialVersionUID=1L; 110 Specialty4() { 111 super("src/facade/WyImage/Specialty14.jpg"); 112 } 113 } 114 115 class Specialty5 extends ImageIcon { 116 private static final long serialVersionUID=1L; 117 Specialty5() { 118 super("src/facade/WyImage/Specialty21.jpg"); 119 } 120 } 121 122 class Specialty6 extends ImageIcon { 123 private static final long serialVersionUID=1L; 124 Specialty6() { 125 super("src/facade/WyImage/Specialty22.jpg"); 126 } 127 } 128 129 class Specialty7 extends ImageIcon { 130 private static final long serialVersionUID=1L; 131 Specialty7() { 132 super("src/facade/WyImage/Specialty23.jpg"); 133 } 134 } 135 136 class Specialty8 extends ImageIcon { 137 private static final long serialVersionUID=1L; 138 Specialty8() { 139 super("src/facade/WyImage/Specialty24.jpg"); 140 } 141 }1 public class WySpecialtyFacade { 2 public static void main(String[] args) { 3 JFrame f = new JFrame ("外觀模式: 婺源特產選擇測試"); 4 Container cp = f.getContentPane(); 5 WySpecialty wys = new WySpecialty(); 6 JScrollPane treeView = new JScrollPane(wys.tree); 7 JScrollPane scrollpane = new JScrollPane(wys.label); 8 // 分隔面板 9 JSplitPane splitpane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,true,treeView,scrollpane); 10 splitpane.setDividerLocation(230); // 設置splitpane的分隔線位置 11 splitpane.setOneTouchExpandable(true); // 設置splitpane可以展開或收起 12 13 cp.add(splitpane); 14 f.setSize(650,350); 15 f.setVisible(true); 16 f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 17 } 18 }
程序運行結果如圖 3-18所示:
圖 3-18 婺源特產管理界面的運行結果
3.5.4 外觀模式的場景應用
通常在以下情況下可以考慮使用外觀模式。
1)對分層結構系統構建時,使用外觀模式定義子系統中每層的入口點可以簡化子系統之間的依賴關係。
2)當一個複雜系統的子系統很多時,外觀模式可以為系統設計一個簡單的接口供外界訪問。
3)當客戶端與多個子系統之間存在很大的聯繫時,引入外觀模式可將它們分離,從而提高子系統的獨立性和可移植性。
3.5.5 外觀模式的擴展
在外觀模式中,當增加或移除子系統時需要修改外觀類,這違背了「開閉原則」。如果引入抽象外觀類,則在一定程度上解決了該問題。
其結構圖如圖 3-19 所示:
圖 3-19 引入抽象外觀類的外觀模式的結構圖
3.6 享元(Flyweight)模式
在面向對象程序設計過程中,有時會面臨要創建大量相同或相似對象實例的問題。創建那麼多的對象將會耗費很多的系統資源,它是系統性能提高的一個瓶頸。例如,圍棋和五子棋中的黑白棋子,圖像中的坐標點或顏色,局域網中的路由器、交換機和集線器,教室里的桌子和凳子等。這些對象有很多相似的地方,如果能把它們相同的部分提取出來共享,則能節省大量的系統資源,這就是享元模式的產生背景。
3.6.1 享元模式的定義與特點
享元模式的定義:運用共享技術來有効地支持大量細粒度對象的復用。它通過共享已經存在的又橡來大幅度減少需要創建的對象數量、避免大量相似類的開銷,從而提高系統資源的利用率。
享元模式的主要優點是:相同對象只要保存一份,這降低了系統中對象的數量,從而降低了系統中細粒度對象給內存帶來的壓力。
其主要缺點是:
1)為了使對象可以共享,需要將一些不能共享的狀態外部化,這將增加程序的複雜性。
2)讀取享元模式的外部狀態會使得運行時間稍微變長。
3.6.2 享元模式的結構與實現
享元模式中存在以下兩種狀態:
1)內部狀態,即不會隨着環境的改變而改變的可共享部分;
2)外部狀態,指隨環境改變而改變的不可以共享的部分。享元模式的實現要領就是區分應用中的這兩種狀態,並將外部狀態外部化。下面來分析其基本結構和實現方法。
(1)模式的結構
享元模式的主要角色有如下:
1)抽象享元(Flyweight)角色:是所有的具體享元類的基類,為具體享元規範需要實現的公共接口,非享元的外部狀態以參數的形式通過方法傳入。
2)具體享元(Concrete Flyweight)角色:實現抽象享元角色中所規定的接口。
3)非享元(Unsharable Flyweight)角色:是不可以共享的外部狀態,它以參數的形式注入具體享元的相關方法中。
4)享元工廠(Flyweight Factory)角色:負責創建和管理享元角色。當客戶對象請求一個享元對象時,享元工廠檢査系統中是否存在符合要求的享元對象,如果存在則提供給客戶;如果不存在的話,則創建一個新的享元對象。
如圖 3-20 所示,是享元模式的結構圖,圖中的 UnsharedConcreteFlyweight 是與淳元角色,裏面包含了非共享的外部狀態信息 info;
而 Flyweight 是抽象享元角色,裏面包含了享元方法 operation(UnsharedConcreteFlyweight state),非享元的外部狀態以參數的形式通過該方法傳入;
ConcreteFlyweight 是具體享元角色,包含了關鍵字 key,它實現了抽象享元接口;
FlyweightFactory 是享元工廠角色,它逝關鍵字 key 來管理具體享元;
客戶角色通過享元工廠獲取具體享元,並訪問具體享元的相關方法。
圖 3-20 享元模式的結構圖
(2)模式的實現
享元模式的實現代碼如下:
1 // 非享元角色 2 class UnsharedConcreteFlyweight { 3 private String info; 4 5 UnsharedConcreteFlyweight(String info) { 6 this.info=info; 7 } 8 9 public String getInfo() { 10 return info; 11 } 12 13 public void setInfo(String info) { 14 this.info=info; 15 } 16 } 17 18 // 抽象享元角色 19 interface Flyweight { 20 public void operation(UnsharedConcreteFlyweight state); 21 } 22 23 // 具體享元角色 24 class ConcreteFlyweight implements Flyweight { 25 private String key; 26 27 ConcreteFlyweight(String key) { 28 this.key = key; 29 System.out.println("具體享元" + key + "被創建!"); 30 } 31 32 public void operation(UnsharedConcreteFlyweight outState) { 33 System.out.print("具體享元" + key + "被調用,"); 34 System.out.println("非享元信息是:" + outState.getInfo()); 35 } 36 } 37 38 // 享元工廠角色 39 class FlyweightFactory { 40 private HashMap<String, Flyweight> flyweights = new HashMap<String, Flyweight>(); 41 42 public Flyweight getFlyweight(String key) { 43 Flyweight flyweight=(Flyweight)flyweights.get(key); 44 45 if(flyweight != null) { 46 System.out.println("具體享元"+key+"已經存在,被成功獲取!"); 47 } else { 48 flyweight=new ConcreteFlyweight(key); 49 flyweights.put(key, flyweight); 50 } 51 return flyweight; 52 } 53 }
1 public class FlyweightPattern { 2 public static void main(String[] args) { 3 FlyweightFactory factory=new FlyweightFactory(); 4 Flyweight f01 = factory.getFlyweight("a"); 5 Flyweight f02 = factory.getFlyweight("a"); 6 Flyweight f03 = factory.getFlyweight("a"); 7 8 Flyweight f11 = factory.getFlyweight("b"); 9 Flyweight f12 = factory.getFlyweight("b"); 10 11 f01.operation(new UnsharedConcreteFlyweight("第1次調用a。")); 12 f02.operation(new UnsharedConcreteFlyweight("第2次調用a。")); 13 f03.operation(new UnsharedConcreteFlyweight("第3次調用a。")); 14 15 f11.operation(new UnsharedConcreteFlyweight("第1次調用b。")); 16 f12.operation(new UnsharedConcreteFlyweight("第2次調用b。")); 17 } 18 }
程序運行結果如下:
具體享元a被創建!
具體享元a已經存在,被成功獲取!
具體享元a已經存在,被成功獲取!
具體享元b被創建!
具體享元b已經存在,被成功獲取!
具體享元a被調用,非享元信息是:第1次調用a。
具體享元a被調用,非享元信息是:第2次調用a。
具體享元a被調用,非享元信息是:第3次調用a。
具體享元b被調用,非享元信息是:第1次調用b。
具體享元b被調用,非享元信息是:第2次調用b。
3.6.3 享元模式的應用實例
【例】享元模式在五子棋遊戲中的應用。
分析:五子棋同圍棋一樣,包含多個「黑」或「白」顏色的棋子,所以用享元模式比較好。
本實例中的棋子(ChessPieces)類是抽象享元角色,它包含了一個落子的 DownPieces(Graphics g,Point pt) 方法;白子(WhitePieces)和黑子(BlackPieces)類是具體享元角色,它實現了落子方法;Point 是非享元角色,它指定了落子的位置;WeiqiFactory 是享元工廠角色,它通過 ArrayList 來管理棋子,並且提供了獲取白子或者黑子的 getChessPieces(String type) 方法;客戶類(Chessboard)利用 Graphics 組件在框架窗體中繪製一個棋盤,並實現 mouseClicked(MouseEvent e) 事件處理方法,該方法根據用戶的選擇從享元工廠中獲取白子或者黑子並落在棋盤上。圖 3-21 所示是其結構圖。
圖 3-21 五子棋遊戲的結構圖
程序代碼如下:
1 // 棋盤 2 class Chessboard extends MouseAdapter { 3 WeiqiFactory wf; 4 JFrame frame; 5 Graphics g; 6 JRadioButton wz; 7 JRadioButton bz; 8 private final int x = 50; 9 private final int y = 50; 10 private final int w = 40; // 小方格寬度和高度 11 private final int rw = 400; // 棋盤寬度和高度 12 13 Chessboard() { 14 wf = new WeiqiFactory(); 15 16 frame = new JFrame("享元模式在五子棋遊戲中的應用"); 17 frame.setBounds(100,100,500,550); 18 frame.setVisible(true); 19 frame.setResizable(false); 20 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 21 22 JPanel SouthJP = new JPanel(); 23 frame.add("South",SouthJP); 24 25 wz = new JRadioButton("白子"); 26 bz = new JRadioButton("黑子", true); 27 28 ButtonGroup group = new ButtonGroup(); 29 group.add(wz); 30 group.add(bz); 31 32 SouthJP.add(wz); 33 SouthJP.add(bz); 34 35 JPanel CenterJP = new JPanel(); 36 CenterJP.setLayout(null); 37 CenterJP.setSize(500, 500); 38 CenterJP.addMouseListener(this); 39 frame.add("Center", CenterJP); 40 41 try { 42 Thread.sleep(500); 43 } catch(InterruptedException e) { 44 e.printStackTrace(); 45 } 46 47 g = CenterJP.getGraphics(); 48 g.setColor(Color.BLUE); 49 g.drawRect(x, y, rw, rw); 50 for(int i=1; i<10; i++) { 51 // 繪製第i條豎直線 52 g.drawLine(x+(i*w), y, x+(i*w), y+rw); 53 // 繪製第i條水平線 54 g.drawLine(x, y+(i*w), x+rw, y+(i*w)); 55 } 56 } 57 58 public void mouseClicked(MouseEvent e) { 59 Point pt = new Point(e.getX() - 15, e.getY() - 15); 60 if(wz.isSelected()) { 61 ChessPieces c1=wf.getChessPieces("w"); 62 c1.DownPieces(g,pt); 63 } else if(bz.isSelected()) { 64 ChessPieces c2=wf.getChessPieces("b"); 65 c2.DownPieces(g,pt); 66 } 67 } 68 } 69 70 // 抽象享元角色:棋子 71 interface ChessPieces { 72 public void DownPieces(Graphics g, Point pt); //下子 73 } 74 75 // 具體享元角色:白子 76 class WhitePieces implements ChessPieces { 77 public void DownPieces(Graphics g, Point pt) { 78 g.setColor(Color.WHITE); 79 g.fillOval(pt.x,pt.y,30,30); 80 } 81 } 82 83 // 具體享元角色:黑子 84 class BlackPieces implements ChessPieces { 85 public void DownPieces(Graphics g, Point pt) { 86 g.setColor(Color.BLACK); 87 g.fillOval(pt.x, pt.y, 30, 30); 88 } 89 } 90 91 // 享元工廠角色 92 class WeiqiFactory { 93 private ArrayList<ChessPieces> qz; 94 95 public WeiqiFactory() { 96 qz = new ArrayList<ChessPieces>(); 97 ChessPieces w = new WhitePieces(); 98 qz.add(w); 99 ChessPieces b = new BlackPieces(); 100 qz.add(b); 101 } 102 103 public ChessPieces getChessPieces(String type) { 104 if(type.equalsIgnoreCase("w")) { 105 return (ChessPieces)qz.get(0); 106 } else if(type.equalsIgnoreCase("b")) { 107 return (ChessPieces)qz.get(1); 108 } else { 109 return null; 110 } 111 } 112 }1 public class WzqGame { 2 public static void main(String[] args) { 3 new Chessboard(); 4 } 5 }
程序運行結果如下圖所示:
圖 3-22 五子棋遊戲的運行結果
3.6.4 享元模式的應用場景
前面分析了享元模式的結構與特點,下面分析它適用的應用場景。享元模式是通過減少內存中對象的數量來節省內存空間的,所以以下幾種情形適合採用享元模式。
1)系統中存在大量相同或相似的對象,這些對象耗費大量的內存資源。
2)大部分的對象可以按照內部狀態進行分組,且可將不同部分外部化,這樣每一個組只需保存一個內部狀態。
3)由於享元模式需要額外維護一個保存享元的數據結構,所以應當在有足夠多的享元實例時才值得使用享元模式。
3.6.5 享元模式的擴展
在前面介紹的享元模式中,其結構圖通常包含可以共享的部分和不可以共享的部分。在實際使用過程中,有時候會稍加改變,即存在兩種特殊的享元模式:單純享元模式和複合享元模式,下面分別對它們進行簡單介紹。
(1)單純享元模式,這種享元模式中的所有的具體享元類都是可以共享的,不存在非共享的具體享元類,其結構圖如圖 3-23 所示。
圖3-23 單享模式的結構圖
(2)複合享元模式,這種享元模式中的有些享元對象是由一些單純享元對象組合而成的,它們就是複合享元對象。雖然複合享元對象本身不能共享,但它們可以分解成單純享元對象再被共享,其結構圖如圖 3-24 所示。
圖 3-24 複合享元模式的結構圖
3.7 組合(Composite)模式
在現實生活中,存在很多「部分-整體」的關係,例如,大學中的部門與學院、總公司中的部門與分公司、學習用品中的書與書包、生活用品中的衣月艮與衣櫃以及廚房中的鍋碗瓢盆等。在軟件開發中也是這樣,例如,文件系統中的文件與文件夾、窗體程序中的簡單控件與容器控件等。對這些簡單對象與複合對象的處理,如果用組合模式來實現會很方便。
3.7.1 組合模式的定義與特點
組合(Composite)模式的定義:有時又叫作部分-整體模式,它是一種將對象組合成樹狀的層次結構的模式,用來表示「部分-整體」的關係,使用戶對單個對象和組合對象具有一致的訪問性。
組合模式的主要優點有:
1)組合模式使得客戶端代碼可以一致地處理單個對象和組合對象,無須關心自己處理的是單個對象,還是組合對象,這簡化了客戶端代碼;
2)更容易在組合體內加入新的對象,客戶端不會因為加入了新的對象而更改源代碼,滿足「開閉原則」;
其主要缺點是:
1)設計較複雜,客戶端需要花更多時間理清類之間的層次關係;
2)不容易限制容器中的構件;
3)不容易用繼承的方法來增加構件的新功能;
3.7.2 組合模式的結構與實現
組合模式的結構不是很複雜,下面對它的結構和實現進行分析。
(1)模式的結構
組合模式包含以下主要角色。
1)抽象構件(Component)角色:它的主要作用是為樹葉構件和樹枝構件聲明公共接口,並實現它們的默認行為。在透明式的組合模式中抽象構件還聲明訪問和管理子類的接口;在安全式的組合模式中不聲明訪問和管理子類的接口,管理工作由樹枝構件完成。
2)樹葉構件(Leaf)角色:是組合中的葉節點對象,它沒有子節點,用於實現抽象構件角色中 聲明的公共接口。
3)樹枝構件(Composite)角色:是組合中的分支節點對象,它有子節點。它實現了抽象構件角色中聲明的接口,它的主要作用是存儲和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。
組合模式分為透明式的組合模式和安全式的組合模式。
① 透明方式:在該方式中,由於抽象構件聲明了所有子類中的全部方法,所以客戶端無須區別樹葉對象和樹枝對象,對客戶端來說是透明的。但其缺點是:樹葉構件本來沒有 Add()、Remove() 及 GetChild() 方法,卻要實現它們(空實現或拋異常),這樣會帶來一些安全性問題。
其結構圖如圖 3-25 所示。
圖 3-25 透明式的組合模式的結構圖
② 安全方式:在該方式中,將管理子構件的方法移到樹枝構件中,抽象構件和樹葉構件沒有對子對象的管理方法,這樣就避免了上一種方式的安全性問題,但由於葉子和分支有不同的接口,客戶端在調用時要知道樹葉對象和樹枝對象的存在,所以失去了透明性。
其結構圖如圖 3-26 所示。
圖 3-36 安全式4的組合模式的結構圖
(2)模式的實現
假如要訪問集合 c0 = {leaf1, {leaf2, leaf3}} 中的元素,其對應的樹狀圖如圖 3-37 所示。
圖 3-37 集合 c0 的樹狀圖
下面給出透明式的組合模式的實現代碼,與安全式的組合模式的實現代碼類似,只要對其做簡單修改就可以了。
1 // 抽象構件 2 interface Component { 3 public void add(Component c); 4 public void remove(Component c); 5 public Component getChild(int i); 6 public void operation(); 7 } 8 9 / / 樹葉構件 10 class Leaf implements Component { 11 private String name; 12 13 public Leaf(String name) { 14 this.name=name; 15 } 16 17 public void add(Component c){} 18 19 public void remove(Component c){} 20 21 public Component getChild(int i) { 22 return null; 23 } 24 25 public void operation() { 26 System.out.println("樹葉" + name + ":被訪問!"); 27 } 28 } 29 30 // 樹枝構件 31 class Composite implements Component { 32 private ArrayList<Component> children = new ArrayList<Component>(); 33 34 public void add(Component c) { 35 children.add(c); 36 } 37 38 public void remove(Component c) { 39 children.remove(c); 40 } 41 42 public Component getChild(int i) { 43 return children.get(i); 44 } 45 46 public void operation() { 47 for(Object obj:children) { 48 ((Component)obj).operation(); 49 } 50 } 51
1 public class CompositePattern { 2 3 public static void main(String[] args) { 4 Component c0 = new Composite(); 5 6 // 添加樹葉 7 Component leaf1 = new Leaf("1"); 8 c0.add(leaf1); 9 10 // 添加樹枝 11 Component c1 = new Composite(); 12 c0.add(c1); 13 14 Component leaf2=new Leaf("2"); 15 Component leaf3=new Leaf("3"); 16 c1.add(leaf2); 17 c1.add(leaf3); 18 19 c0.operation(); 20 } 21 }
程序運行結果如下:
樹葉1:被訪問!
樹葉2:被訪問!
樹葉3:被訪問!
3.7.3 組合模式的應用實例
【例】用組合模式實現當用戶在商店購物後,顯示其所選商品信息,並計算所選商品總價的功能。
說明:假如李先生到韶關「天街e角」生活用品店購物:用 1 個紅色小袋子裝了 2 包婺源特產(單價 7.9 元)、1 張婺源地圖(單價 9.9 元);
用 1 個白色小袋子裝了 2 包韶關香藉(單價 68 元)和 3 包韶關紅茶(單價 180 元);
用 1 個中袋子裝了前面的紅色小袋子和 1 個景德鎮瓷器(單價 380 元);
用 1 個大袋子裝了前面的中袋子、白色小袋子和 1 雙李寧牌運動鞋(單價 198 元)。
最後「大袋子」中的內容有:
{
1 雙李寧牌運動鞋(單價 198 元),
白色小袋子 {
2 包韶關香菇(單價 68 元),3 包韶關紅茶(單價 180 元)
},
中袋子 {
1 個景德鎮瓷器(單價 380 元),
紅色小袋子 {
2 包婺源特產(單價 7.9 元),1 張婺源地圖(單價 9.9 元)
}
}
},
現在要求編程顯示李先生放在大袋子中的所有商品信息並計算要支付的總價。
本實例可按安全組合模式設計,其結構圖如圖 3-38 所示。
圖 3-38 韶關「天街e角」店購物的結構圖
程序代碼如下:
1 // 抽象構件:物品 2 interface Articles { 3 public float calculation(); //計算 4 public void show(); 5 } 6 7 // 樹葉構件:商品 8 class Goods implements Articles { 9 private String name; // 名字 10 private int quantity; // 數量 11 private float unitPrice; // 單價 12 13 public Goods(String name, int quantity, float unitPrice) { 14 this.name = name; 15 this.quantity = quantity; 16 this.unitPrice = unitPrice; 17 } 18 19 public float calculation() { 20 return quantity * unitPrice; 21 } 22 23 public void show() { 24 System.out.println(name + "(數量:" + quantity + ",單價:" + unitPrice + "元)"); 25 } 26 } 27 28 // 樹枝構件:袋子 29 class Bags implements Articles { 30 private String name; // 名字 31 private ArrayList<Articles> arrBags = new ArrayList<>(); 32 33 public Bags(String name) { 34 this.name = name; 35 } 36 37 public void add(Articles c) { 38 arrrBags.add(c); 39 } 40 41 public void remove(Articles c) { 42 arrBags.remove(c); 43 } 44 45 public Articles getChild(int i) { 46 return arrBags.get(i); 47 } 48 49 public float calculation() { 50 float s = 0; 51 for(Object obj : arrBags) { 52 s += ((Articles)obj).calculation(); 53 } 54 return s; 55 } 56 57 public void show() { 58 for(Object obj : arrBags) { 59 ((Articles)obj).show(); 60 } 61 } 62 }1 public class ShoppingTest { 2 public static void main(String[] args) { 3 Bags BigBag, mediumBag, smallRedBag, smallWhiteBag; 4 Goods sp; 5 6 smallRedBag = new Bags("紅色小袋子"); 7 sp = new Goods("婺源特產", 2, 7.9f); 8 smallRedBag.add(sp); 9 sp = new Goods("婺源地圖", 1, 9.9f); 10 smallRedBag.add(sp); 11 12 smallWhiteBag = new Bags("白色小袋子"); 13 sp = new Goods("韶關香菇", 2, 68); 14 smallWhiteBag.add(sp); 15 sp = new Goods("韶關紅茶", 3, 180); 16 smallWhiteBag.add(sp); 17 18 mediumBag = new Bags("中袋子"); 19 sp = new Goods("景德鎮瓷器", 1, 380); 20 mediumBag.add(sp); 21 mediumBag.add(smallRedBag); 22 23 BigBag = new Bags("大袋子"); 24 sp = new Goods("李寧牌運動鞋", 1, 198); 25 BigBag.add(sp); 26 BigBag.add(smallWhiteBag); 27 BigBag.add(mediumBag); 28 29 System.out.println("您選購的商品有:"); 30 BigBag.show(); 31 32 float s = BigBag.calculation(); 33 System.out.println("要支付的總價是:" + s + "元"); 34 } 35 }
程序運行結果如下:
您選購的商品有: 李寧牌運動鞋(數量:1,單價:198.0元) 韶關香菇(數量:2,單價:68.0元) 韶關紅茶(數量:3,單價:180.0元) 景德鎮瓷器(數量:1,單價:380.0元) 婺源特產(數量:2,單價:7.9元) 婺源地圖(數量:1,單價:9.9元) 要支付的總價是:1279.7元
3.7.4 組合模式的應用場景
前面分析了組合模式的結構與特點,下面分析它適用的以下應用場景。
1)在需要表示一個對象整體與部分的層次結構的場合。
2)要求對用戶隱藏組合對象與單個對象的不同,用戶可以用統一的接口使用組合結構中的所有對象的場合。
3.7.5 組合模式的擴展
如果對前面介紹的組合模式中的樹葉節點和樹枝節點進行抽象,也就是說樹葉節點和樹枝節點還有子節點,這時組合模式就擴展成複雜的組合模式了,如 Java AWT/Swing 中的簡單組件 JTextComponent 有子類 JTextField、JTextArea,容器組件 Container 也有子類 Window、Panel。複雜的組合模式的結構圖如圖 3-39 所示。
圖 3-39 複雜的組合模式的結構圖