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

来源:https://www.cnblogs.com/yychuyu/archive/2020/07/11/13283491.html
-Advertisement-
Play Games

封裝變化之介面隔離 在組件的構建過程當中,某些介面之間直接的依賴常常會帶來很多問題、甚至根本無法實現。採用添加一層間接(穩定)的介面,來隔離本來互相緊密關聯的介面是一種常見的解決方案。 這裡的介面隔離不同於介面隔離原則,介面隔離原則是對介面職責隔離,也就是儘量減少介面職責,使得一個類對另一個類的依賴 ...


封裝變化之介面隔離

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

這裡的介面隔離不同於介面隔離原則,介面隔離原則是對介面職責隔離,也就是儘量減少介面職責,使得一個類對另一個類的依賴應該建立在最小的介面上。

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

外觀模式 - Facade

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

在我們對系統進行研究的時候,往往會採用抽象與分解的思路去簡化系統的複雜度,因此在這個過程當中就將一個複雜的系統劃分成為若幹個子系統。也正是因為如此,子系統之間的通信與相互依賴也就增加了,為了使得這種依賴達到最小,Facade模式正好可以解決這種問題。

Facade模式體現的更多的是一種介面隔離的思想,它體現在很多方面上,最常見的比如說用戶圖形界面、操作系統等。這都可以體現這樣一個思想。

facade.png

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模式中,我們創建具有現有對象的代理對象,以便向外界提供功能介面。其目的在於為其他對象提供一種代理以控制對這個對象的訪問。

這是因為一個對象的創建和初始化可能會產生很大的開銷,這也就意味著我們可以在真正需要這個對象時再對其進行相應的創建和初始化。

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

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

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • NEST教程系列:三種推斷索引名寫法 連接時設置預設索引 構建 ConnectionSettings 時調用 DefaultIndex() 方法指定預設索引名。當無法為請求解析具體索引名時使用預設索引名 var settings = new ConnectionSettings() .Default ...
  • 場景 ASP.NET中新建Web網站並部署到IIS上(詳細圖文教程): https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/107199747 在上面博客中已經將網站部署到了IIS上。 .NET Framework 為了保證數據的安全性 ...
  • 場景 ASP.NET中新建Web網站並部署到IIS上(詳細圖文教程): https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/107199747 在上面博客中已經將網站部署到了IIS上。 註: 博客: https://blog.csdn. ...
  • MVC控制器向視圖傳遞數據包含多個實體類的解決方案有很多,這裡主要針對視圖模型、動態模型以及Tuple三種方法進行一些總結與記錄。 基礎集合A namespace ViewModelStudy.Models{ public class TableA { public int A { get; set ...
  • 首先分析了C#的數據類型,以及值類型和引用類型的區別,最後介紹了三種類型轉換的函數,以及他們的區別和使用場景,主要是對小數點的出來規則 ...
  • 今天給大家分享一下Windows Terminal的使用及個性化定製。 ...
  • 每次發現系統變慢時,我們通常做的第一件事,就是執行 top 或者 uptime 命令,來瞭解系統的負載情況。比如像下麵這樣,我在命令行里輸入了 uptime 命令,系統也隨即給出了結果。 $ uptime ​ 02:34:03 up 2 days, 20:14, 1 user, load avera ...
  • 什麼是hosts文件? hosts文件是一個用於儲存電腦網路中各節點信息的電腦文件 這個文件負責將主機名映射到相應的IP地址 hosts文件通常用於補充或取代網路中DNS的功能 和DNS不同的是電腦的用戶可以直接對hosts文件進行控制 why do it 今天在給Mac裝Homebrew的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...