從封裝變化的角度看設計模式——接口隔離

封裝變化之接口隔離

在組件的構建過程中,某些接口之間直接的依賴經常會帶來不少問題、甚至根本沒法實現。採用添加一層間接(穩定)的接口,來隔離原本互相緊密關聯的接口是一種常見的解決方案。java

這裏的接口隔離不一樣於接口隔離原則,接口隔離原則是對接口職責隔離,也就是儘可能減小接口職責,使得一個類對另外一個類的依賴應該創建在最小的接口上。程序員

而這裏所講到的接口隔離是對依賴或者通訊關係的隔離,經過在原有系統中加入一個層次,使得整個系統的依賴關係大大的下降。而這樣的模式主要有外觀模式、代理模式、中介者模式和適配器模式。面試

外觀模式 - Facade

Facade模式其主要目的在於爲子系統中的一組接口提供一個一致的界面(接口),Facade模式定義了一個高層接口,這個接口使得更加容易使用。安全

在咱們對系統進行研究的時候,每每會採用抽象與分解的思路去簡化系統的複雜度,所以在這個過程中就將一個複雜的系統劃分紅爲若干個子系統。也正是由於如此,子系統之間的通訊與相互依賴也就增長了,爲了使得這種依賴達到最小,Facade模式正好能夠解決這種問題。架構

Facade模式體現的更多的是一種接口隔離的思想,它體如今不少方面上,最多見的好比說用戶圖形界面、操做系統等。這均可以體現這樣一個思想。框架

facade.png

Facade模式從結構上能夠簡化爲上面這樣一種形式,但其形式並不固定,尤爲是體如今其內部子系統的關係上,由於其內部的子系統關係確定是複雜多樣的,而且SubSystem不必定是類或者對象,也有多是一個模塊,這裏只是用類圖來表現Facade模式與其子系統之間的關係。ide

從代碼體現上來看,能夠這樣表現:學習

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模式的實現帶來很大的影響。this

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模式中,咱們建立具備現有對象的代理對象,以便向外界提供功能接口。其目的在於爲其餘對象提供一種代理以控制對這個對象的訪問。

這是由於一個對象的建立和初始化可能會產生很大的開銷,這也就意味着咱們能夠在真正須要這個對象時再對其進行相應的建立和初始化。

好比在文件系統中對一個圖片的訪問,當咱們以列表形式查看文件時,並不須要顯示整個圖片的信息,只有在選中圖片的時候,纔會顯示其預覽信息,再在雙擊以後可能纔會真正打個這個圖片,這時可能才須要從磁盤當中加載整個圖片信息。

ProxyImage.png

對圖片代理的理解就如同上面的結構圖同樣,在文件欄中預覽時,只是顯示代理對象當中的fileName等信息,而代理對象當中的image信息只會在真正須要Image對象的時候纔會創建實線指向的聯繫。

經過上面的例子,能夠清楚的看到代理模式在訪問對象時,引入了必定程度的間接性,這種間接性根據不一樣的狀況能夠附加相應的具體處理。

好比,對於遠程代理對象,能夠隱藏一個對象不存在於不一樣地址空間的事實。對於虛代理對象,能夠根據要求建立對象、加強對象功能等等。還有保護代理對象,能夠爲對象的訪問增長權限控制。

這一系列的代理都體現了代理模式的高擴展性。但同時也會增長代理開銷,因爲在客戶端和真實主題之間增長了代理對象,所以有些類型的代理模式可能會形成請求的處理速度變慢。而且實現代理模式須要額外的工做,有些代理模式的實現很是複雜。

對於上面的例子,能夠用類圖更加詳細地闡述。

Proxy.png

在這樣一個結構中,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;若是沒有塔臺的協調,那麼各個航班飛機的起降將只能由航班飛機之間造成一個多對多(一對多)的通訊網來控制,這種控制必然是及其複雜的;可是有了塔臺的加入,整個系統就簡化了許多,全部的航班只須要和塔臺進行通訊,也只須要接收來自塔臺的控制便可完成全部任務。這就使得多對多(一對多)的關係轉化成了一對一的關係。

mediator.png

看到中介者模式類圖的時候,有沒有發覺好像和哪一個模式有點類似,有沒有點像觀察者模式。

之因此如此類似的緣由就是觀察者模式和中介者模式都涉及到了對象狀態變化與狀態通知這兩個過程。觀察者模式當中,目標(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。

也就是說,如今的狀況能夠用下圖來進行描述:

adapter-before .png

這時候,爲了右邊的系統Player 融入到右邊中,就能夠採用適配器模式。

adapter-object .png

經過增長一個適配器,並將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();
    }
}複製代碼

以前說了除了對象適配器以外,還有類適配器。而類適配器若是要實現就須要適配中的適配者是一個已經實現的結構,若是沒有實現還須要適配者本身實現,這種實現方式就致使其靈活性沒有對象適配器那麼高。

adapter-class.png其類圖就是上面這樣一種形式,主要區別體如今適配器的實現,而其部分變化不大。

//類適配器
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學習路線圖,因而我根據本身的經驗,利用業餘時間熬夜肝了一個月,整理了一份電子書。不管你是面試仍是自我提高,相信都會對你有幫助!目錄以下:

免費送給你們,只求你們金指給我點個贊!

電子書 | Linux開發學習路線圖

也但願有小夥伴能加入我,把這份電子書作得更完美!

有收穫?但願老鐵們來個三連擊,給更多的人看到這篇文章

推薦閱讀:

相關文章
相關標籤/搜索