從封裝變化的角度看設計模式——接口隔離
封裝變化之接口隔離
在組件的構建過程當中,某些接口之間直接的依賴常常會帶來很多問題、甚至根本無法實現。採用添加一層間接(穩定)的接口,來隔離本來互相緊密關聯的接口是一種常見的解決方案。
這裏的接口隔離不同於接口隔離原則,接口隔離原則是對接口職責隔離,也就是儘量減少接口職責,使得一個類對另一個類的依賴應該建立在最小的接口上。
而這裏所講到的接口隔離是對依賴或者通信關係的隔離,通過在原有系統中加入一個層次,使得整個系統的依賴關係大大的降低。而這樣的模式主要有外觀模式、代理模式、中介者模式和適配器模式。
外觀模式 - Facade
Facade模式其主要目的在於爲子系統中的一組接口提供一個一致的界面(接口),Facade模式定義了一個高層接口,這個接口使得更加容易使用。
在我們對系統進行研究的時候,往往會採用抽象與分解的思路去簡化系統的複雜度,因此在這個過程當中就將一個複雜的系統劃分成爲若干個子系統。也正是因爲如此,子系統之間的通信與相互依賴也就增加了,爲了使得這種依賴達到最小,Facade模式正好可以解決這種問題。
Facade模式體現的更多的是一種接口隔離的思想,它體現在很多方面上,最常見的比如說用戶圖形界面、操作系統等。這都可以體現這樣一個思想。
Facade模式從結構上可以簡化爲上面這樣一種形式,但其形式並不固定,尤其是體現在其內部子系統的關係上,因爲其內部的子系統關係肯定是複雜多樣的,並且 SubSystem
不一定是類或者對象,也有可能是一個模塊,這裏只是用類圖來表現Facade模式與其子系統之間的關係。
從代碼體現上來看,可以這樣表現:
public class SubSystem1 { public void operation1(){ //完成子系統1的功能 ...... } } public class SubSystem2 { public void operation2(){ //完成子系統2的功能 ...... } } public class SubSystem3 { public void operation3(){ //完成子系統3的功能 ...... } } public class SubSystem21 extends SubSystem2{ //對子系統2的擴展 ...... } public class SubSystem22 extends SubSystem2 { //對子系統2的擴展 ...... }
上面子系統內部各部分的一個體現,如何結合Facade來對外隔離它的系統內部複雜依賴呢?看下面:
public class Facade { private SubSystem1 subSystem1; private SubSystem2 subSystem2; private SubSystem3 subSystem3; public Facade(){ subSystem1 = new SubSystem1(); subSystem2 = new SubSystem21(); subSystem3 = new SubSystem3(); } public void useSystem1(){ subSystem1.operation1(); } public void useSystem2(){ subSystem2.operation2(); } public void useSystem3(){ subSystem3.operation3(); } }
當然,這只是Facade模式的一種簡單實現,可能在真正的實現系統中,會有着更加複雜的實現,比如各子系統之間可能存在依賴關係、又或者調用各子系統時需要傳遞參數等等,這些都會給Facade模式的實現帶來很大的影響。
public class Client { public static void main(String[] args) { Facade facade = new Facade(); facade.useSystem1(); facade.useSystem2(); facade.useSystem3(); } }
當存在Facade之後,客戶對子系統的訪問就只需要面對Facade,而不需要再去理解各子系統之間的複雜依賴關係。當然對於普通客戶而言,使用Facade所提供的接口自然是足夠的;對於更加高級的客戶而言,Facade模式並未屏蔽高級客戶對子系統的訪問,也就是說,如果有客戶需要根據子系統定製自己的功能也是可以的。
對Facade的理解很簡單,但是在具體使用時,又需要注意些什麼呢?
- 進一步地降低客戶與子系統之間的耦合度。
具體實現是,使用抽象類來實現Facade而通過它的具體子類來應對不同子系統的實現,並且可以滿足客戶根據要求自己定製Facade。
除了使用子類的方式之外,通過其他的子系統來配置Facade也是一個方法,並且這種方法的靈活性更好。
- 在層次化結構中,可以使用外觀模式定義系統中每一層的入口。 剛纔我們就提到過,
SubSystem
不一定只表示一個類,它包含的可能是一些類,並且是一些具有協作關係的類,那麼對於這些類,自然也是使用外觀模式來爲其定義一個統一的接口。 - Facade模式自身也有缺點,雖然它減少系統的相互依賴,提高靈活性,提高了安全性;但是其本身就是不符合開閉原則的,如果子系統發生變化或者客戶需求變化,就會涉及到Facade的修改,這種修改是很麻煩的,因爲無論是通過擴展或是繼承都可能無法解決,只能以修改源碼的方式。
代理模式 - Proxy
在Proxy模式中,我們創建具有現有對象的代理對象,以便向外界提供功能接口。其目的在於爲其他對象提供一種代理以控制對這個對象的訪問。
這是因爲一個對象的創建和初始化可能會產生很大的開銷,這也就意味着我們可以在真正需要這個對象時再對其進行相應的創建和初始化。
比如在文件系統中對一個圖片的訪問,當我們以列表形式查看文件時,並不需要顯示整個圖片的信息,只有在選中圖片的時候,纔會顯示其預覽信息,再在雙擊之後可能纔會真正打個這個圖片,這時可能才需要從磁盤當中加載整個圖片信息。
對圖片代理的理解就如同上面的結構圖一樣,在文件欄中預覽時,只是顯示代理對象當中的 fileName
等信息,而代理對象當中的 image
信息只會在真正需要Image對象的時候纔會建立實線指向的聯繫。
通過上面的例子,可以清楚的看到代理模式在訪問對象時,引入了一定程度的間接性,這種間接性根據不同的情況可以附加相應的具體處理。
比如,對於遠程代理對象,可以隱藏一個對象不存在於不同地址空間的事實。對於虛代理對象,可以根據要求創建對象、增強對象功能等等。還有保護代理對象,可以爲對象的訪問增加權限控制。
這一系列的代理都體現了代理模式的高擴展性。但同時也會增加代理開銷,由於在客戶端和真實主題之間增加了代理對象,因此有些類型的代理模式可能會造成請求的處理速度變慢。並且實現代理模式需要額外的工作,有些代理模式的實現非常複雜。
對於上面的例子,可以用類圖更加詳細地闡述。
在這樣一個結構中,jpg圖片與圖片代理類共同實現了一個圖片接口,並且在圖片代理類中存放了一個對於 JpgImage
的引用,這個引用在未有真正使用到時,是爲null的,只有在需要使用時,纔對其進行初始化。
//Subject(代理的目標接口) public interface Image { public void show(); public String getInfo(); } //RealSubject(被代理的實體) public class JpgImage implements Image { private String imageInfo; @Override public void show() { //顯示完整圖片 ...... } @Override public String getInfo() { return imageInfo; } public Image loadImage(String fileName){ //從磁盤當中加載圖片信息 // ...... return new JpgImage(); } }
//Proxy(代理類) public class ImageProxy implements Image { private String fileName; private Image image; @Override public void show() { if (image==null){ image = loadImage(fileName); } image.show(); } @Override public String getInfo() { if (image==null){ return fileName; }else{ return image.getInfo(); } } public Image loadImage(String fileName){ //從磁盤當中加載圖片信息 ...... return new JpgImage(); } }
public class Client { public static void main(String[] args) { Image imageProxy = new ImageProxy(); imageProxy.getInfo(); imageProxy.show(); } }
在實際的使用過程上,客戶就可以不再涉及具體的類,而是可以只關注代理類。
代理模式的種類有很多,根據代理的實現形式不同,可以劃分爲:
- 遠程代理:爲一個對象在不同的地址空間提供局部代表。
- 虛代理:爲需要創建開銷很大的對象生成代理。(如上面的實例)
- 保護代理:控制對原始對象的訪問。保護代理主要用於對象應該有不同的保護權限時。
- 智能指引:在訪問對象時執行一些附加的操作。
以上的代理都是靜態代理的形式,爲什麼說是靜態呢,這是因爲在實現的過程中,它的類型都是事先預定好的,比如 ImageProxy
這個類,它就只能代理 Image
的子類。
與靜態相對的自然就產生了動態代理。動態代理中,最主要的兩種方式就是基於JDK的動態代理和基於CGLIB的動態代理。這兩種動態代理也是Spring框架中實現AOP(Aspect Oriented Programming)的兩種動態代理方式。這裏,就不深入了,後面有機會再對動態代理做一個詳細的講解。
中介者模式 - Mediator
中介者模式用一箇中介對象來封裝一系列的對象交互。中介者使各對象不需要顯示地相互引用,從而使其耦合鬆散,而且可以獨立地改變它們之間的交互。
中介者模式產生的一個重要原因就在於,面向對象設計鼓勵將行爲分頁到各個對象中。而這種分佈就可能會導致對象間有許多連接,這些連接就是導致系統複用和修改困難的原因所在。
就比如一個機場調度的實現,在這個功能當中,各個航班就是Colleague,而塔臺就是Mediator;如果沒有塔臺的協調,那麼各個航班飛機的起降將只能由航班飛機之間形成一個多對多(一對多)的通信網來控制,這種控制必然是及其複雜的;但是有了塔臺的加入,整個系統就簡化了許多,所有的航班只需要和塔臺進行通信,也只需要接收來自塔臺的控制即可完成所有任務。這就使得多對多(一對多)的關係轉化成了一對一的關係。
看到中介者模式類圖的時候,有沒有發覺好像和哪個模式有點相似,有沒有點像觀察者模式。
之所以如此相似的原因就是觀察者模式和中介者模式都涉及到了對象狀態變化與狀態通知這兩個過程。觀察者模式當中,目標(Subject)的狀態發生變化就會通知其所有的(Observer);同樣,在中介者模式當中,其相應的同事類(一羣通過中介者相互協作的類)狀態發生變化,就需要通知中介者,再由中介者來處理狀態信息並反饋給其他的同事類。
因此,中介者模式的實現方法之一就是使用觀察者模式,將Mediator作爲一個Observer,各個Colleague作爲Subject,一旦Colleague狀態發生變化就發送通知給Mediator。Mediator作出響應並將狀態改變的結果傳播給其他的Colleague。
另外還有一種方式,是在Mediator中定義一個特殊的接口,各個Colleague直接調用這個接口,並將自己作爲參數傳入,然後由這個接口來選擇將信息發送給誰。
//Mediator public class ControlTower { private List<Flight> flights = new ArrayList<>(); public void addFlight(Flight flight){ flights.add(flight); } public void removeFlight(Flight flight){ flights.remove(flight); } public void control(Flight flight){ //對航班進行起降控制 ...... //如果航班起飛,則從flights移除 //如果航班降落,則加入到flights } }
public class Flight { private ControlTower cTower; public void setcTower(ControlTower cTower) { this.cTower = cTower; } public void changed(){ cTower.control(this); } } public class Flight1 extends Flight{ public void takeOff(){ //起飛操作 ...... } public void land(){ //降落操作 ...... } } public class Flight2 extends Flight{ //起飛 降落 操作 ...... } public class Flight3 extends Flight{ //同樣 起飛 降落 操作 ...... }
那麼客戶怎樣使用這樣一個模式呢?看下面這樣一個操作:
public class Client { public static void main(String[] args) { ControlTower controlTower = new ControlTower(); //假設一個飛機入場要麼是有跑道空閒要麼是另一個飛機起飛 Flight f1 = new Flight1(); f1.setcTower(controlTower); //此時一號機降落, //controlTower調用contorl控制飛機起降 f1.changed(); Flight f2 = new Flight2(); f2.setcTower(controlTower); //此時二號機降落, //controlTower調用contorl控制1號飛機起飛,二號降落 f2.changed(); ....... } }
中介者模式主要解決的是,如果系統中對象之間存在比較複雜的引用關係,導致它們之間的依賴關係結構混亂而且難以複用該對象,就可以使用中介者來簡化依賴關係。但是這也可能會使得中介者會龐大,變得複雜難以維護,所以在使用中介者模式時,儘量是在保持中介者穩定的情況下使用。
適配器模式 - Adapter
適配器的目的在於將一個類的接口轉換成客戶希望的另外一個接口,從而使得原本由於接口不兼容而不能在一起工作的類可以在一起工作。
首先在使用適配器的時候,需要明確的是,適配器不是在詳細設計時添加的,而是解決正在服役的項目的問題。爲什麼,因爲適配器本身就存在一些問題,比如明明我想調用的是一個文件接口,結果傳輸出來的卻是一張圖片,如果系統當中出現太多這樣的情況,那無異會使得系統的應用變得極其困難。
所以只有在系統正在運用,並且重構困難的情況下,才選擇使用適配器來適配接口。
而適配器模式又根據作用對象可以分爲類適配器和對象適配器兩種實現方式。
假設我們現在已經存在一個播放器,這個播放器只能播放mp3格式的音頻。但是現在又出現了一個新的播放器,這個播放器有兩種播放格式mp4和wma。
也就是說,現在的情況可以用下圖來進行描述:
這時候,爲了右邊的系統 Player
融入到右邊中,就可以採用適配器模式。
通過增加一個適配器,並將 player
作爲適配器的一個屬性,當傳入具體的播放器時,就在 newPlay()
中調用 player.play()
。
具體實現如下:
//Adaptee (適配者,要求將這個存在的接口適配成目標的接口) public interface Player { public void play(); } public class Mp3Player implements Player { @Override public void play() { System.out.println("播放mp3格式"); } }
//Target(適配目標,需要適配成那個目標的接口) public interface NewPlayer { public void newPlay(); } public class WmaNewPlayer implements NewPlayer { @Override public void newPlay() { System.out.println("播放wmas格式"); } } public class Mp4NewPlayer implements NewPlayer { @Override public void newPlay() { System.out.println("播放mp4格式"); } }
接下來就是適配器的實現了。
//對象適配器 //首先在適配器中,增加一個適配者(Player)的引用 //然後使用適配者(Player)實現適配目標(NewPlayer)的接口 public class PlayerAdapter implements NewPlayer { private Player player; public PlayerAdapter(Player player){ this.player = player; } @Override public void newPlay() { player.play(); } }
然後整個系統的調用變化爲:
public class Client { public static void main(String[] args) { //播放mp4,wma的形式不變 NewPlayer mp4Player = new Mp4NewPlayer(); mp4Player.newPlay(); NewPlayer wmaPlayer = new WmaNewPlayer(); wmaPlayer.newPlay(); //如果要播放mp3格式,可以使用適配器來進行 Player adapter = new PlayerAdapter(new Mp3Player()); adapter.newPlay(); } }
這樣的一個適配過程可能存在一點不完善的地方,就在於,雖然對兩都進行了適配,但調用方式不統一。爲了統一調用過程,其實還可以做如下修改:
//對象適配器修改爲 //首先在適配器中,增加適配者(newPlayer)和目標(player)的引用 //然後使用適配者(newPlayer)實現適配目標(Player)的接口 public class PlayerAdapter implements NewPlayer { private NewPlayer newPlayer; private Player player; public PlayerAdapter(NewPlayer newPlayer){ this.newPlayer = newPlayer; } public PlayerAdapter(Player layer){ this.player = player; } @Override public void newPlay() { if(player!=null){ player.play(); }else{ newPlayer.newPlay(); } } } //這樣修改適配器之後,客戶類的調用就變成了都通過適配器來進行 public class Client { public static void main(String[] args) { //播放mp3 Player adapter1 = new PlayerAdapter(new Mp3Player()); adapter1.newPlay(); //播放mp4 Player adapter2 = new PlayerAdapter(new Mp4NewPlayer()); adapter2.newPlay(); //播放wma Player adapter3 = new PlayerAdapter(new WmaNewPlayer()); adapter3.newPlay(); } }
之前說了除了對象適配器之外,還有類適配器。而類適配器如果要實現就需要適配中的 適配者是一個已經實現的結構 ,如果沒有實現還需要適配者自己實現,這種實現方式就導致其靈活性沒有對象適配器那麼高。
其類圖就是上面這樣一種形式,主要區別體現在適配器的實現,而其部分變化不大。
//類適配器 public class PlayerAdapter extends Mp3Player implements NewPlayer { @Override public void newPlay() { play(); } } //客戶調用過程就變化爲: public class Client { public static void main(String[] args) { //播放mp4,wma的形式不變 NewPlayer mp4Player = new Mp4NewPlayer(); mp4Player.newPlay(); NewPlayer wmaPlayer = new WmaNewPlayer(); wmaPlayer.newPlay(); //如果要播放mp3格式,可以使用適配器來進行 Player adapter = new PlayerAdapter(); adapter.newPlay(); } }
但是如果Player存在不同子類,那明顯使用對象適配器是更好的選擇。
當然也不是說類適配器就不一定沒有對象適配器之外的優勢。兩者的使用有不同的權衡。
類適配器:
- 用一個具體的Adapater類對Adaptee和Target進行匹配。結果是當我們想要匹配一個類以及所有它的子類時,類適配器就不再適用。
- 因爲Adapter是Adaptee的子類,這就使得Adapter可以重定義Adaptee的部分行爲。
- 不需要再引入對象,不需要引入額外的引用就可以得到adaptee。
對象適配器:
- 允許一個Adapter與多個Adaptee——即Adaptee本身以及它的所有子類同時工作,並且Adapter也可以一次給所有的Adaptee添加功能。
- 想要重定義Adaptee的行爲比較困難,但對於增強Adaptee的功能卻很容易。如果要自定義Adaptee的行爲,就只能生成Adaptee的子類來實現重定義。
最後,最近很多小夥伴找我要 Linux學習路線圖 ,於是我根據自己的經驗,利用業餘時間熬夜肝了一個月,整理了一份電子書。無論你是面試還是自我提升,相信都會對你有幫助!
免費送給大家,只求大家金指給我點個贊!
也希望有小夥伴能加入我,把這份電子書做得更完美!