使用C# (.NET Core) 實現介面卡模式 (Adapter Pattern) 和外觀模式 (Facade Pattern)

solenovex發表於2018-04-16

本文的概念內容來自深入淺出設計模式一書

現實世界中的介面卡(模式)

我帶著一個國標插頭的膝上型電腦, 來到歐洲, 想插入到歐洲標準的牆壁插座裡面, 就需要用中間這個電源介面卡.

物件導向的介面卡

你有個老系統, 現在來了個新供應商的類, 但是它們的介面不同, 如何使用這個新供應商的類呢?

首先, 我們不想修改現有程式碼, 你也不能修改供應商的程式碼. 那麼你只能寫一個可以適配新供應商介面的類了:

這裡, 中間的介面卡實現了你的類所期待的介面, 並且可以和供應商的介面互動以便處理你的請求.

介面卡可以看作是中間人, 它從客戶接收請求, 並把它們轉化為供應商可以理解的請求:

所有的新程式碼都寫在介面卡裡面了.

鴨子的例子

有這麼一句話不知道您聽過沒有: 如果它路像個鴨子, 叫起來也像個鴨子, 那它就是個鴨子. (例如: Python裡面的duck typing)

這句話要是用來形容介面卡模式就得這麼改一下: 如果它走路像個鴨子, 叫起來也像個鴨子, 那麼它可能是一個使用了鴨子介面卡的火雞….

看一下程式碼的實現:

鴨子介面:

namespace AdapterPattern.Abstractions
{
    public interface IDuck
    {
        void Quack();
        void Fly();
    }
}

野鴨子:

using AdapterPattern.Abstractions;

namespace AdapterPattern
{
    public class MallardDuck : IDuck
    {
        public void Fly()
        {
            System.Console.WriteLine("Flying");
        }

        public void Quack()
        {
            System.Console.WriteLine("Quack");
        }
    }
}

火雞介面:

namespace AdapterPattern.Abstractions
{
    public interface ITurkey
    {
        void Gobble();
        void Fly();
    }
}

野火雞:

using AdapterPattern.Abstractions;

namespace AdapterPattern.Turkies
{
    public class WildTurkey : ITurkey
    {
        public void Fly()
        {
            System.Console.WriteLine("Gobble gobble");
        }

        public void Gobble()
        {
            System.Console.WriteLine("I`m flying a short distance");
        }
    }
}

火雞介面卡:

using AdapterPattern.Abstractions;

namespace AdapterPattern.Adapters
{
    public class TurkeyAdapter : IDuck
    {
        private readonly ITurkey turkey;

        public TurkeyAdapter(ITurkey turkey)
        {
            this.turkey = turkey;
        }

        public void Fly()
        {
            for (int i = 0; i < 5; i++)
            {
                turkey.Fly();
            }
        }

        public void Quack()
        {
            turkey.Gobble();
        }
    }
}

測試執行:

using System;
using AdapterPattern.Abstractions;
using AdapterPattern.Adapters;
using AdapterPattern.Turkies;

namespace AdapterPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            DuckTestDrive();
        }

        static void DuckTestDrive()
        {
            IDuck duck = new MallardDuck();
            var turkey = new WildTurkey();
            IDuck turkeyAdapter = new TurkeyAdapter(turkey);

            System.Console.WriteLine("Turkey says.........");
            turkey.Gobble();
            turkey.Fly();

            System.Console.WriteLine("Duck says.........");
            TestDuck(duck);

            System.Console.WriteLine("TurkeyAdapter says.........");
            TestDuck(turkeyAdapter);
        }

        static void TestDuck(IDuck duck)
        {
            duck.Quack();
            duck.Fly();
        }
    }
}

這個例子很簡單, 就不解釋了.

理解介面卡模式

Client 客戶實現了某種目標介面, 它傳送請求到介面卡, 介面卡也實現了該介面, 並且介面卡保留著被適配者的例項, 介面卡把請求轉化為可以在被適配者身上執行的一個或者多個動作.

客戶並不知道有介面卡做著翻譯的工作.

其他:

介面卡可以適配兩個或者多個被適配者.

介面卡也可以是雙向的, 只需要實現雙方相關的介面即可.

介面卡模式定義

介面卡模式把一個類的介面轉化成客戶所期待的另一個介面. 介面卡讓原本因介面不相容而無法一起工作的類成功的工作在了一起.

類圖:

其中 Client只知道目標介面, 介面卡實現了這個目標介面, 介面卡是通過組合的方式與被適配者結合到了一起, 所有的請求都被委託給了被適配者.

物件介面卡和類介面卡

一共有兩類介面卡: 物件介面卡類介面卡.

之前的例子都是物件介面卡.

為什麼沒有提到類介面卡? 

因為類介面卡需要多繼承, 這一點在Java和C#裡面都是不可以的. 但是其他語言也許可以例如C++?

它的類圖是這樣的:

這個圖看著也很眼熟, 這兩種介面卡唯一的區別就是: 類介面卡同時繼承於目標和被適配者, 而物件介面卡使用的是組合的方式來把請求傳遞給被適配者.

通過鴨子的例子來認識兩種介面卡的角色

類介面卡:

類介面卡裡面, 客戶認為它在和鴨子談話, 目標就是鴨子類, 客戶呼叫鴨子上面的方法. 火雞沒有和鴨子一樣的方法, 但是介面卡可以接收鴨子的方法呼叫並把該動作轉化為呼叫火雞上面的方法. 介面卡讓火雞可以響應一個針對於鴨子的請求, 實現方法就是同時繼承於鴨子類和火雞類

物件介面卡:

物件介面卡裡, 客戶仍然認為它在和鴨子說話, 目標還是鴨子類, 客戶呼叫鴨子類的方法, 介面卡實現了鴨子類的介面, 但是當它接收到方法呼叫的時候, 它把該動作轉化委託給了火雞. 火雞並沒有實現和鴨子一樣的介面, 多虧了介面卡, 火雞(被適配者)將會接收到客戶針對鴨子介面的方法呼叫.

兩種介面卡比較:

物件介面卡: 使用組合的方式, 不僅能是配一個被適配者的類, 還可以適配它的任何一個子類.

類介面卡: 只能適配一個特定的類, 但是它不需要重新實現整個被適配者的功能. 而且它還可以重寫被適配者的行為.

物件介面卡: 我使用的是組合而不是繼承, 我通過多寫幾行程式碼把事情委託給了被適配者. 這樣很靈活.

類介面卡: 你需要一個介面卡和一個被適配者, 而我只需要一個類就行.

物件介面卡: 我對介面卡新增的任何行為對被適配者和它的子類都起作用.

又一個介面卡的例子 (Java)

 老版本的java有個介面叫做Enumeration:

後來又出現了一個Iterator介面:

現在我想把Enumeration適配給Iterator:

這個應該很簡單, 可以這樣設計:

只有一個問題, Enumeration不支援remove動作, 也就是說介面卡也無法讓remove變成可能, 所以只能這樣做: 丟擲一個不支援該操作的異常(C#: NotSupportedException), 這也就是介面卡也無法做到完美的地方.

看一下這個java介面卡的實現:

裝飾模式 vs 介面卡模式

你可能發現了, 這兩個模式有一些相似, 那麼看看它們之間的對話:

裝飾模式: 我的工作全都是關於職責, 使用我的時候, 肯定會涉及到在設計裡新增新的職責或行為.

介面卡模式: 我主要是用來轉化介面.

裝飾模式: 當我裝飾一個大號介面的時候, 真需要寫很多程式碼.

介面卡模式: 想把多個類整合然後提供給客戶所需的介面, 這也是很麻煩的工作. 但是熟話說: “解耦的客戶都是幸福的客戶…”

裝飾模式: 用我的時候, 我也不知道已經套上多少了裝飾器了.

介面卡模式: 介面卡幹活的時候, 客戶也不知道我們的存在. 但是我們允許客戶在不修改現有程式碼的情況下使用新的庫, 靠我們來轉化就行.

裝飾模式: 我們只允許為類新增新的行為, 而無需修改現有程式碼.

介面卡模式: 所以說, 我們總是轉化我們所包裹的介面.

裝飾模式: 我們則是擴充套件我們包裝的物件, 為其新增行為或職責.

從這段對話可以看出, 裝飾模式和介面卡模式的根本區別就是它們的意圖不同.

 

另一種情況

現在我們可以知道, 介面卡模式會把類的介面轉化成客戶所需要的樣子.

但是還有另外一種情況也需要轉化介面, 但卻處於不同的目的: 簡化介面. 這就需要使用外觀模式(Facade Pattern). 

外觀模式會隱藏一個或多個類的複雜性, 並提供一個整潔乾淨的外觀(供外界使用).

現在在總結一下這三種模式的特點:

裝飾者模式: 不修改介面, 但是新增職責.

介面卡模式: 把一個介面轉化成另外一個.

外觀模式: 把介面變得簡單.

 

一個需求 — 家庭影院

這個家庭影院有DVD播放器, 投影儀, 螢幕, 環繞立體音響, 還有個爆米花機:

你可能花了幾周的時間去連線, 組裝…..現在你想看一個電影, 步驟如下:

  1. 開啟爆米花機
  2. 開始製作爆米花
  3. 把燈光調暗
  4. 把螢幕放下來
  5. 把投影儀開啟
  6. 把投影儀的輸入媒介設為DVD
  7. 把投影儀調整為寬屏模式
  8. 開啟功放
  9. 把功放的輸入媒介設為DVD
  10. 把功放設定為環繞立體聲
  11. 把功放的音量調到中檔
  12. 把DVD播放器開啟
  13. 開始播放DVD

具體用程式描述就是這樣的:

  • 目前一共是這些步驟?…但是還沒完:
  • 當電影播放完了, 得把所有的裝置關掉, 那麼反向重新操作一遍?
  • 要是聽CD或者收音機是不是也同樣的麻煩??
  • 如果系統升級, 那麼你還得學習一遍新的流程吧?

這個需求, 就需要使用外觀模式了.

使用外觀模式, 你可以通過實現一個外觀類把一個複雜的子系統簡單化, 因為這個外觀類會提供一個更合理的介面.

HomeTheaterFacade這個外觀類只暴露了幾個簡單的方法, 例如看電影watchMovie(). 這個外觀類把整個家庭影院看作是它的子系統, 通過呼叫子系統的相關方法來實現對外的簡單方法.

而客戶只需要呼叫這些簡單的方法即可. 但是外觀類的子系統仍然保留著對外界的可直接訪問性, 如果你需要高階功能, 就可以直接呼叫.

以下幾點需要理解:

  • 外觀類並沒有”封裝”子系統, 它只不過是對子系統的功能額外提供了一套簡單的方法. 外界仍然可以直接訪問子系統的方法.
  • 外觀類也可以新增額外的功能.
  • 針對一個子系統可以建立若干個外觀類
  • 外觀模式讓客戶和子系統解耦.
  • 介面卡模式是把一個或多個類的介面轉化成客戶所需要的一個介面, 而外觀模式則是提供了一個簡單的介面. 它們之間的根本不同是它們的目的, 介面卡是要改變介面, 以便可以符合客戶要求; 外觀模式則是為子系統提供一個簡化的介面.

程式碼:

這裡只貼HomeTheaterFacade的程式碼吧, 其他的東西太簡單了 (其他程式碼在這裡: https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp):

using System;
using FacadePattern.Equipments;

namespace FacadePattern.Facades
{
    public class HomeTheaterFacade
    {
        private readonly Amplifier _amp;
        private readonly DvdPlayer _dvd;
        private readonly Projector _projector;
        private readonly TheaterLights _lights;
        private readonly Screen _screen;
        private readonly PopcornPopper _popper;

        public HomeTheaterFacade(Amplifier amp, DvdPlayer dvd, Projector projector, TheaterLights lights, Screen screen, PopcornPopper popper)
        {
            _amp = amp;
            _dvd = dvd;
            _projector = projector;
            _lights = lights;
            _screen = screen;
            _popper = popper;
        }

        public void WatchMovie()
        {
            Console.WriteLine("Get ready to watch a movie");

            _popper.On();
            _popper.Pop();
            _lights.Dim(5);
            _screen.Down();
            _projector.On();
            _projector.WideScreenMode();
            _amp.On();
            _amp.SetDvd();
            _amp.SetSurroundSound();
            _amp.SetVolume(5);
            _dvd.On();
            _dvd.Play("Ready Player One");
        }

        public void EndMovie()
        {
            Console.WriteLine("Shutting movie theater down...");
            _popper.Off();
            _lights.On();
            _screen.Up();
            _projector.Off();
            _amp.Off();
            _dvd.Stop();
            _dvd.Eject();
            _dvd.Off();
        }
    }
}

測試:

using FacadePattern.Equipments;
using FacadePattern.Facades;

namespace FacadePattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var facade = new HomeTheaterFacade(new Amplifier(), new DvdPlayer(), new Projector(), new TheaterLights(), new Screen(), new PopcornPopper());
            facade.WatchMovie("Ready Player One");
            facade.EndMovie();
        }
    }
}

OK.

外觀模式定義

外觀模式為擁有一套介面的子系統提供了一個為一個簡化介面. 外觀類定義了一個高階介面, 這個介面可以使子系統用起來更簡單.

設計原則 — 不要知道的太多

不要知道的太多, 只和你最近的朋友談話.

意思是說, 當你設計系統的時候, 對於任何一個物件, 要小心它所互動的類的個數, 並知道這些類是怎麼來的..

一個問題, 下面這段程式碼耦合了多少個類?

這個原則有一些指導性建議, 在某個物件的任何一個方法裡面, 我們只可以呼叫屬於下列物件的方法:

  • 物件本身
  • 從該方法引數傳進來的物件
  • 該方法建立或者例項化的物件
  • 物件的元件

如果呼叫一個另一個方法返回的物件會有什麼害處?

這樣做就是向另一個物件的子部件進行請求, 同時也增加了我們直接接觸的物件的個數.

所以不遵循規則的程式碼是這樣的:

遵循規則的應該是這樣的:

保持在界內呼叫方法

看這個例子:

engine是這個類的元件, 可以呼叫它的start()方法 .

start()方法裡key是傳進去的, 可以呼叫key的方法.

doors是我們在方法內建立的物件, 可以呼叫doors的方法.

可以呼叫本類的updateDashboardDisplay()方法.

這個原則的缺點就是: 它可能會需要很多的包裝類來處理其他元件的方法呼叫., 這樣會提高複雜度也會增加開發時的時間.

再看下面兩個寫法:

第一種是”錯誤”的, 第二種是正確的.

外觀模式和少知道原則

Client 客戶只有一個朋友, HomeTheaterFacade.

HomeTheaterFacade管理子系統裡面的元件以便客戶可以簡單靈活的使用.

如果升級系統, 並不會影響客戶

儘量讓子系統符合少知道原則, 有必要的話可以引進多層外觀.

總結

少知道原則: 只跟最近的朋友講話.

介面卡模式: 轉化一個類的介面以便客戶可以使用.

外觀模式: 為一個子系統的一套介面提供一個統一的介面. 外觀定義了一個讓子系統更容易使用的高階介面.

 

C#原始碼: https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp

相關文章