本文的概念內容來自深入淺出設計模式一書 現實世界中的適配器(模式) 我帶著一個國標插頭的筆記本電腦, 來到歐洲, 想插入到歐洲標準的牆壁插座裡面, 就需要用中間這個電源適配器. 面向對象的適配器 你有個老系統, 現在來了個新供應商的類, 但是它們的介面不同, 如何使用這個新供應商的類呢? 首先, 我 ...
本文的概念內容來自深入淺出設計模式一書
現實世界中的適配器(模式)
我帶著一個國標插頭的筆記本電腦, 來到歐洲, 想插入到歐洲標準的牆壁插座裡面, 就需要用中間這個電源適配器.
面向對象的適配器
你有個老系統, 現在來了個新供應商的類, 但是它們的介面不同, 如何使用這個新供應商的類呢?
首先, 我們不想修改現有代碼, 你也不能修改供應商的代碼. 那麼你只能寫一個可以適配新供應商介面的類了:
這裡, 中間的適配器實現了你的類所期待的介面, 並且可以和供應商的介面交互以便處理你的請求.
適配器可以看作是中間人, 它從客戶接收請求, 並把它們轉化為供應商可以理解的請求:
所有的新代碼都寫在適配器裡面了.
鴨子的例子
有這麼一句話不知道您聽過沒有: 如果它路像個鴨子, 叫起來也像個鴨子, 那它就是個鴨子. (例如: 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播放器, 投影儀, 屏幕, 環繞立體音響, 還有個爆米花機:
你可能花了幾周的時間去連線, 組裝.....現在你想看一個電影, 步驟如下:
- 打開爆米花機
- 開始製作爆米花
- 把燈光調暗
- 把屏幕放下來
- 把投影儀打開
- 把投影儀的輸入媒介設為DVD
- 把投影儀調整為寬屏模式
- 打開功放
- 把功放的輸入媒介設為DVD
- 把功放設置為環繞立體聲
- 把功放的音量調到中檔
- 把DVD播放器打開
- 開始播放DVD
具體用程式描述就是這樣的:
- 目前一共是這些步驟