設計模式--結構型模式
參考:尚矽谷,學校課件
介面卡模式:
1)介面卡模式將某個類的介面轉換成客戶端期望的另一個介面表示,主的目的是相容性,讓原本因介面不匹配不能一起工作的兩個類可以協同工作。其別名為包裝器
2) 介面卡模式屬於結構型模式
3) 主要分為三類:類介面卡模式、物件介面卡模式、介面介面卡模式
類介面卡:
示例程式碼:
//============= //Target介面,就是我們想把其他類的功能放到這個介面 public interface Robot { public void cry(); public void move(); } //===================== //別人的類,假裝不懂怎麼實現,相當於不適合咱們的原裝插座,怎沒能力把它改成合適的,只能看見插口(方法的名稱,引數,返回值)public class Dog { public void wang() { System.out.println("狗汪汪叫!"); } public void run() { System.out.println("狗快快跑!"); } } //=============== //介面卡登場了,這是類介面卡,繼承未知類 public class DogAdapter extends Dog implements Robot { public void cry() { System.out.print("機器人模仿:"); super.wang(); } public void move() { System.out.print("機器人模仿:"); super.run(); } } //=============== //使用者端使用 public class Client { public static void main(String args[]) { Robot robot=new DogAdapter(); robot.cry(); robot.move(); } }
物件介面卡:
程式碼分析:
//=============== //物件是配置器,與上面程式碼唯一不同就是介面卡不繼承未知類,而是把他的物件引用當成自身屬性 public class Adapter extends Target { private Adaptee adaptee; public Adapter(Adaptee adaptee) { this.adaptee=adaptee; } public void request() { adaptee.specificRequest(); } }
介面介面卡:
原理:通過抽象類來實現適配。
當存在這樣一個介面,其中定義了N多的方法,而我們現在卻只想使用其中的一個到幾個方法,如果我們直接實現介面,那麼我們要對所有的方法進行實現,哪怕我們僅僅是對不需要的方法進行置空(只寫一對大括號,不做具體方法實現)也會導致這個類變得臃腫,呼叫也不方便,這時我們可以使用一個抽象類作為中介軟體,即介面卡,用這個抽象類實現介面,而在抽象類中所有的方法都進行置空,那麼我們在建立抽象類的繼承類,而且重寫我們需要使用的那幾個方法即可。
橋接模式:
將抽象部分與它的實現部分分離,使它們都可以獨立地變化。
情形分析:
對於毛筆而言,由於型號是其固有的維度,因此可以設計一個抽象的毛筆類,在該類中宣告並部分實現毛筆的業務方法,而將各種型號的毛筆作為其子類;顏色是毛筆的另一個維度,由於它與毛筆之間存在一種“設定”的關係,因此我們可以提供一個抽象的顏色介面,而將具體的顏色作為實現該介面的子類。在此,型號可認為是毛筆的抽象部分,而顏色是毛筆的實現部分,見上圖結構示意圖。
現需要提供大中小3種型號的畫筆,能夠繪製5種不同顏色,如果使用蠟筆,我們需要準備3*5=15支蠟筆,也就是說必須準備15個具體的蠟筆類。而如果使用毛筆的話,只需要3種型號的毛筆,外加5個顏料盒,用3+5=8個類就可以實現15支蠟筆的功能。本例項使用橋接模式來模擬毛筆的使用過程。
示例程式碼:
//============ //這次我們先看使用時的樣子 public class Client { public static void main(String a[]) { Color color;//是介面,有不同顏色的實現類,所謂實現部分 Pen pen;//是個抽象類,有大小型別的子類實現,所謂抽象部分 color=new Blue(); pen=new BigPen(); pen.setColor(color);//為毛筆選擇顏料 pen.draw("鮮花");//使用毛筆 } } //============== //pen介面,裡面有Color引用 public abstract class Pen { protected Color color; public void setColor(Color color) { this.color=color; } public abstract void draw(String name); } //================ //bigPen的實現,想在draw中不光體現"大號",也想體現顏色,所以在draw()方法實際呼叫內部的Color實現,這樣能同時體現這兩項內容 public class BigPen extends Pen { public void draw(String name) { String penType="大號毛筆繪製"; this.color.bepaint(penType,name); } } //============== //color的介面 public interface Color { void bepaint(String penType,String name); } //================== //Blue顏料的實現 public class Blue implements Color { public void bepaint(String penType,String name) { System.out.println(penType + "藍色的"+ name + "."); } }
橋接模式注意事項和細節:
1) 實現了抽象和實現部分的分離,從而極大的提供了系統的靈活性,讓抽象部分和實 現部分獨立開來,這有助於系統進行分層設計,從而產生更好的結構化系統。
2) 對於系統的高層部分,只需要知道抽象部分和實現部分的介面就可以了,其它的部 分由具體業務來完成。
3) 橋接模式替代多層繼承方案,可以減少子類的個數,降低系統的管理和維護成本。
4) 橋接模式的引入增加了系統的理解和設計難度,由於聚合關聯關係建立在抽象層, 要求開發者針對抽象進行設計和程式設計
5) 橋接模式要求正確識別出系統中兩個獨立變化的維度,因此其使用範圍有一定的局 限性,即需要有這樣的應用場景。
組合模式:
模式動機:
對於樹形結構,當容器物件(如資料夾)的某一個方法被呼叫時,將遍歷整個樹形結構,尋找也包含這個方法的成員物件(可以是容器物件,也可以是葉子物件,如子資料夾和檔案)並呼叫執行。(遞迴呼叫)
由於容器物件和葉子物件在功能上的區別,在使用這些物件的客戶端程式碼中必須有區別地對待容器物件和葉子物件,而實際上大多數情況下我們希望一致地處理它們,因為對於這些物件的區別對待將會使得程式非常複雜。
模式結構:
例項:水果盤
在水果盤(Plate)中有一些水果,如蘋果(Apple)、香蕉(Banana)、梨子(Pear),當然大水果盤中還可以有小水果盤,現需要對盤中的水果進行遍歷(吃),當然如果對一個水果盤執行“吃”方法,實際上就是吃其中的水果。使用組合模式模擬該場景。
程式碼結構示例如下:
分析上面的結構,各種水果和盤子都繼承了MyElement物件,盤子有List<MyElement > list物件,盤子的add()接受的是MyElement介面型別,也就是可以盤子裝水果,也可以盤子裡套盤子。盤子的eat()方法,就是呼叫盤子裡裝的list元素的eat()方法,如果是水果直接吃,如果是盤子就吃裡面的水果,這兩種行為再外部來看都是MyElement.eat()
程式碼示例:
//==================== //MyElement,水果與盤子共同的方法 public abstract class MyElement { public abstract void eat(); } //=============== //水果比較簡單,直接吃就可以了 public class Apple extends MyElement { public void eat() { System.out.println("吃蘋果!"); } } //=============== //盤子除了吃(呼叫盤子裝的元素),還要對盤中元素有基本的管理操作 public class Plate extends MyElement { private ArrayList list=new ArrayList(); public void add(MyElement element) { list.add(element); } public void delete(MyElement element) { list.remove(element); } public void eat() { for(Object object:list) { ((MyElement)object).eat(); } } } //==================== //站在使用者端,吃就完事了 public class Client { public static void main(String a[]) { MyElement obj1,obj2,obj3,obj4,obj5; Plate plate1,plate2,plate3; obj1=new Apple(); obj2=new Pear(); plate1=new Plate(); plate1.add(obj1); plate1.add(obj2); obj3=new Banana(); obj4=new Banana(); plate2=new Plate(); plate2.add(obj3); plate2.add(obj4); obj5=new Apple(); plate3=new Plate(); plate3.add(plate1); plate3.add(plate2); plate3.add(obj5); plate1.eat(); } }
組合模式的注意事項和細節
1) 簡化客戶端操作。客戶端只需要面對一致的物件而不用考慮整體部分或者節點葉子 的問題。
2) 具有較強的擴充套件性。當我們要更改組合物件時,我們只需要調整內部的層次關係, 客戶端不用做出任何改動.
3) 方便創建出複雜的層次結構。客戶端不用理會組合裡面的組成細節,容易新增節點 或者葉子從而創建出複雜的樹形結構
4) 需要遍歷組織機構,或者處理的物件具有樹形結構時, 非常適合使用組合模式.
5) 要求較高的抽象性,如果節點和葉子有很多差異性的話,比如很多方法和屬性 都不一樣,不適合使用組合模式
裝飾模式:
模式動機:
在軟體設計中,一般有兩種方式可以實現給一個類或物件增加行為(新功能):
繼承機制,使用繼承機制是給現有類新增功能的一種有效途徑,通過繼承一個現有類可以使得子類在擁有自身方法的同時還擁有父類的方法。但是這種方法是靜態的,使用者不能控制增加行為的方式和時機。
關聯機制,即將一個類的物件嵌入另一個物件中,由另一個物件來呼叫嵌入物件的行為同時擴充套件其行為,我們稱這另一個物件為裝飾器(Decorator)。
裝飾器使用物件之間的關聯關係取代類之間的繼承關係,在裝飾器中既可以呼叫原有類的方法,還可以增加新的方法,以擴充原有類的功能。它通過一種無須定義子類的方式來給物件動態增加職責,符合合成複用原則。
分析下上圖結構,很明顯類似介面卡,ConcreteComponent類似一個固定的類,只不過裝飾模式下我們想的是對該類的功能進行增強,將裝飾類和該類繼承同一個抽象類,然後裝飾類內部有父類引用(用來指向固定類),然後又裝飾類的子類進行個性化定製。
例項:變形金剛
變形金剛在變形之前是一輛汽車,它可以在陸地上移動。當它變成機器人之後除了能夠在陸地上移動之外,還可以說話;如果需要,它還可以變成飛機,除了在陸地上移動還可以在天空中飛翔。
結構解析:
程式碼示例:
//============== //要魔改的方法,用介面用抽象類都一樣,,吧 public interface Transform { public void move(); } //=============== //開始固定的類,也就是基礎小汽車形態 //Car()是建立的時候輸出一下 public final class Car implements Transform { public Car() { System.out.println("變形金剛是一輛車!"); } public void move() { System.out.println("在陸地上移動!"); } } //====================== //這裡的裝飾器保留了move()原功能沒有動,只是加入了額外的功能 public class Changer implements Transform { private Transform transform; public Changer(Transform transform) { this.transform=transform;//為了使用Car的方法 } public void move() { transform.move(); } } //=============== //變身小飛機,多加了一個fly()方法 public class Airplane extends Changer { public Airplane(Transform transform) { super(transform); System.out.println("變成飛機!"); } public void fly() { System.out.println("在天空飛翔!"); } } //================= //客戶端使用 public class Client { public static void main(String args[]) { Transform camaro; camaro=new Car(); camaro.move(); System.out.println("-----------------------------"); Airplane bumblebee=new Airplane(camaro); bumblebee.move();//注意這裡變成飛機還可以在地上跑 bumblebee.fly(); } }
外觀模式:
在現實生活中,存在如下一些場景:
想組裝一臺新電腦,一個方案是去電子市場把自己需要的配件都買回來,然後自己組裝,但需要對各種配件都要比較熟悉,這樣才能選擇最合適的配件,而且還要考慮配件之間的相容性;另外一個方案,就是到電子市場,找一家專業裝機的公司,把具體的要求一講,然後就等著拿電腦就好了,方案二不需要挨家去跑,也不需要熟悉各種配件,通常是大多數人的選擇。
我們把組裝電腦的過程抽象一下:如果把電子市場看成是一個系統,而各個賣配件的公司看成是子系統的話,就類似於出現了這樣一種情況:客戶端為了完成某個功能,需要去呼叫某個系統中的多個子系統,簡稱為子系統A、子系統B和子系統C,對於客戶端而言,那就需要知道A、B、C這三個子系統的功能,還需要知道如何組合A、B、C這三個子系統所提供的功能來實現自己所需要的功能,非常麻煩。
方案二提供了一種簡單的方法,能讓客戶端實現相同的功能卻不需要跟系統中的多個模組互動。
模式結構:
示例:電器總開關
就是把一些亂七八糟的小物件綜合起來,放到一個GeneralSwitchFacade(遙控器)中,點下開關全部開啟,關上開關全部關閉。
程式碼實現:
//================ //電扇 public class Fan { public void on() { System.out.println("風扇開啟!"); } public void off() { System.out.println("風扇關閉!"); } } //=============== //空調 public class AirConditioner { public void on() { System.out.println("空調開啟!"); } public void off() { System.out.println("空調關閉!"); } } //==================== //燈組 public class Light { private String position; public Light(String position) { this.position=position; } public void on() { System.out.println(this.position + "燈開啟!"); } public void off() { System.out.println(this.position + "燈關閉!"); } } //============= //接下來就是萬眾期待的遙控器了 public class GeneralSwitchFacade { private Light lights[]=new Light[4]; private Fan fan; private AirConditioner ac; private Television tv; public GeneralSwitchFacade() { lights[0]=new Light("左前"); lights[1]=new Light("右前"); lights[2]=new Light("左後"); lights[3]=new Light("右後"); fan=new Fan(); ac=new AirConditioner(); tv=new Television(); } public void on() { lights[0].on(); lights[1].on(); lights[2].on(); lights[3].on(); fan.on(); ac.on(); tv.on(); } public void off() { lights[0].off(); lights[1].off(); lights[2].off(); lights[3].off(); fan.off(); ac.off(); tv.off(); } } //=================== //客戶端使用 public class Client { public static void main(String args[]) { GeneralSwitchFacade gsf=new GeneralSwitchFacade(); gsf.on(); System.out.println("-----------------------"); gsf.off(); } }
外觀模式的注意事項和細節
1) 外觀模式對外遮蔽了子系統的細節,因此外觀模式降低了客戶端對子系統使用的複雜性
2) 外觀模式對客戶端與子系統的耦合關係,讓子系統內部的模組更易維護和擴充套件
3) 通過合理的使用外觀模式,可以幫我們更好的劃分訪問的層次
4) 當系統需要進行分層設計時,可以考慮使用Facade模式
5) 在維護一個遺留的大型系統時,可能這個系統已經變得非常難以維護和擴充套件,此時 可以考慮為新系統開發一個Facade類,來提供遺留系統的比較清晰簡單的介面, 讓新系統與Facade類互動,提高複用性
6) 不能過多的或者不合理的使用外觀模式,使用外觀模式好,還是直接呼叫模組好。 要以讓系統有層次,利於維護為目的。
享元模式:
模式動機:
現要開發一圍棋軟體,通過分析發現,圍棋棋盤中包含大量的黑子和白子,它們的形狀、大小都一模一樣,只是出現的位置不同而已。如果將每一個棋子都作為一個獨立的物件儲存在記憶體中,將導致該圍棋軟體在執行時所需記憶體空間較大。如何實現對這些相同或者相似物件的共享訪問,從而節約記憶體並提高系統性能?
享元模式正是為解決這一類問題而誕生的。享元模式通過共享技術實現相同或相似物件的重用。
再看看軟體開發中常見的字串,比如一個文字字串中往往存在很多重複的字元,如果每一個字元都用一個單獨的物件來表示,將會佔用較多的記憶體空間。
這時就可以通過享元模式實現相同或相似物件的重用。在邏輯上每一個出現的字元都有一個物件與之對應,然而在物理上它們卻共享同一個享元物件,這個物件可以出現在一個字串的不同地方,相同的字元物件都指向同一個例項。
在享元模式中,儲存這些共享例項物件的地方稱為享元池(Flyweight Pool)。我們可以針對每一個不同的字元建立一個享元物件,將其放在享元池中,需要時再從享元池取出。
享元模式以共享的方式高效地支援大量細粒度物件的重用,享元物件能做到共享的關鍵是區分了內部狀態(Intrinsic State)和外部狀態(Extrinsic State)。下面將對享元的內部狀態和外部狀態進行簡單的介紹:
內部狀態是儲存在享元物件內部並且不會隨環境改變而改變的狀態,內部狀態可以共享。如字元的內容,不會隨外部環境的變化而變化,無論在任何環境下字元“a”始終是“a”,都不會變成“b”。
外部狀態是隨環境改變而改變的、不可以共享的狀態。享元物件的外部狀態通常由客戶端儲存,並在享元物件被建立之後,需要使用的時候再傳入到享元物件內部。一個外部狀態與另一個外部狀態之間是相互獨立的。如字元的顏色,可以在不同的地方有不同的顏色,例如有的“a”是紅色的,有的“a”是綠色的,字元的大小也是如此,有的“a”是五號字,有的“a”是四號字。而且字元的顏色和大小是兩個獨立的外部狀態,它們可以獨立變化,相互之間沒有影響,客戶端可以在使用時將外部狀態注入享元物件中。
正因為區分了內部狀態和外部狀態,我們可以將具有相同內部狀態的物件儲存在享元池中,享元池中的物件是可以實現共享的,需要的時候就將物件從享元池中取出,實現物件的複用。通過向取出的物件注入不同的外部狀態,可以得到一系列相似的物件,而這些物件在記憶體中實際上只儲存一份。
經典享元模式分析:
Flyweight: 抽象享元類,通常是一個介面或抽象類,在抽象享元類中聲明瞭具體享元類公共的方法,這些方法可以向外界提供享元物件的內部資料(內部狀態),同時也可以通過這些方法來設定外部資料(外部狀態)。
ConcreteFlyweight: 具體享元類,實現了抽象享元類,其例項稱為享元物件;在具體享元類中為內部狀態提供了儲存空間。通常我們可以結合單例模式來設計具體享元類,為每一個具體享元類提供唯一的享元物件。
UnsharedConcreteFlyweight: 非共享具體享元類,並不是所有的抽象享元類的子類都需要被共享,不能被共享的子類可設計為非共享具體享元類;當需要一個非共享具體享元類的物件時可以直接通過例項化建立。
FlyweightFactory: 享元工廠類,用於建立並管理享元物件,它針對抽象享元類程式設計,將各種型別的具體享元物件儲存在一個享元池中,享元池一般設計為一個儲存“鍵值對”的集合(也可以是其他型別的集合),可以結合工廠模式進行設計;當用戶請求一個具體享元物件時,享元工廠提供一個儲存在享元池中已建立的例項或者建立一個新的例項(如果不存在的話),返回新建立的例項並將其儲存在享元池中。
享元模式的核心在於享元工廠類,在享元模式中引入了享元工廠類,享元工廠類的作用在於提供一個用於儲存享元物件的享元池,當用戶需要物件時,首先從享元池中獲取,如果享元池中不存在,則建立一個新的享元物件返回給使用者,並在享元池中儲存該新增物件。
例項:共享網路裝置
很多網路裝置都是支援共享的,如交換機、集線器等,多臺終端計算機可以連線同一臺網絡裝置,並通過該網路裝置進行資料轉發,如圖所示,現用享元模式模擬共享網路裝置的設計原理。
雖然網路裝置可以共享,但是分配給每一個終端計算機的埠(Port)是不同的,因此多臺計算機雖然可以共享同一個網路裝置,但必須使用不同的埠。我們可以將埠從網路裝置中抽取出來作為外部狀態,需要時再進行設定。
示例結構分析;
實現程式碼:
//================== //網路設別介面 //getType()返回是集線器還是交換機,因為這個介面被這兩種裝置繼承,所以型別算內部狀態,故沒有引數也能得到 //use(Port port) 不管是集線器還是交換機,都要讓計算機接入,還需要介面,但一個網路裝置要供好多計算機使用,如果把port當做成員變數,需要List<Port> 好不好另說,本題的意思把Port當做外部狀態,所以用作引數。 public interface NetworkDevice { public String getType(); public void use(Port port); } //=============== //port比較簡單 public class Port { private String port; public Port(String port) { this.port=port; } public void setPort(String port) { this.port=port; } public String getPort() { return this.port; } } //=============== //交換機的實現 public class Switch implements NetworkDevice { private String type; public Switch(String type) { this.type=type; } public String getType() { return this.type; } public void use(Port port) { System.out.println("Linked by switch, type is " + this.type + ", port is " + port.getPort()); } } //============== //集線器 public class Hub implements NetworkDevice { private String type; public Hub(String type) { this.type=type; } public String getType() { return this.type; } public void use(Port port) { System.out.println("Linked by Hub, type is " + this.type + ", port is " + port.getPort()); } } //================== //最重要的網路裝置工廠,它實現了享元模式,只例項化了兩個網路裝置,然後用這兩種網路裝置實現所有計算機的接入,並且標註埠號,還記錄網路裝置和計算機(沒呼叫一次網路裝置工廠表示有一個計算機接入)的個數 public class DeviceFactory { private ArrayList devices = new ArrayList(); private int totalTerminal=0; public DeviceFactory() { NetworkDevice nd1=new Switch("Cisco-WS-C2950-24"); devices.add(nd1); NetworkDevice nd2=new Hub("TP-LINK-HF8M"); devices.add(nd2); } public NetworkDevice getNetworkDevice(String type) { if(type.equalsIgnoreCase("cisco")) { totalTerminal++; return (NetworkDevice)devices.get(0); } else if(type.equalsIgnoreCase("tp")) { totalTerminal++; return (NetworkDevice)devices.get(1); } else { return null; } } public int getTotalDevice() { return devices.size(); } public int getTotalTerminal() { return totalTerminal; } } //================ //客戶端的例項 public class Client { public static void main(String args[]) { NetworkDevice nd1,nd2,nd3,nd4,nd5; DeviceFactory df=new DeviceFactory(); nd1=df.getNetworkDevice("cisco"); nd1.use(new Port("1000")); nd2=df.getNetworkDevice("cisco"); nd2.use(new Port("1001")); nd3=df.getNetworkDevice("cisco"); nd3.use(new Port("1002")); nd4=df.getNetworkDevice("tp"); nd4.use(new Port("1003")); nd5=df.getNetworkDevice("tp"); nd5.use(new Port("1004")); System.out.println("Total Device:" + df.getTotalDevice()); System.out.println("Total Terminal:" + df.getTotalTerminal()); } }
享元模式的注意事項和細節
1) 在享元模式這樣理解,“享”就表示共享,“元”表示物件
2) 系統中有大量物件,這些物件消耗大量記憶體,並且物件的狀態大部分可以外部化時, 我們就可以考慮選用享元模式
3) 用唯一標識碼判斷,如果在記憶體中有,則返回這個唯一標識碼所標識的物件,用 HashMap/HashTable儲存
4) 享元模式大大減少了物件的建立,降低了程式記憶體的佔用,提高效率
5) 享元模式提高了系統的複雜度。需要分離出內部狀態和外部狀態,而外部狀態具有 固化特性,不應該隨著內部狀態的改變而改變,這是我們使用享元模式需要注意的地方.
6) 使用享元模式時,注意劃分內部狀態和外部狀態,並且需要有一個工廠類加以控制。
7) 享元模式經典的應用場景是需要緩衝池的場景,比如 String常量池、資料庫連線池
享元模式與單例模式的比較:
首先,享元模式解決的是減少大量小物件的記憶體開銷,單例模式解決的是某個類的例項在程式中只需要一個。其次,享元模式和單例模式,雖然都是通過共享物件來解決,但是享元模式中的物件是有外部狀態的,比如,在文字編輯軟體中,對字元的處理,在檔案中會出現大量相同的字母,相同這是物件的內部狀態,而相同的字母在不同位置有時他的顏色、背景色等等,不一樣,這是外部狀態,通常外部狀態是通過方法傳遞過來的。
在說下這兩個模式的程式碼吧
代理模式通常使用懶漢方式
而享元模式,可以看做是單例模式+工廠模式+合成模式