——.NET設計模式系列之八
Terrylee,2006年2月
概述
在軟體系統中,由于應用環境的變化,常常需要将“一些現存的對象”放在新的環境中應用,但是新環境要求的接口是這些現存對象所不滿足的。那麼如何應對這種“遷移的變化”?如何既能利用現有對象的良好實作,同時又能滿足新的應用環境所要求的接口?這就是本文要說的Adapter 模式。
意圖
将一個類的接口轉換成客戶希望的另外一個接口。Adapter模式使得原本由于接口不相容而不能一起工作的那些類可以一起工作。
結構圖
圖1 類的Adapter模式結構圖
圖2 對象的Adapter模式結構圖
生活中的例子
擴充卡模式允許将一個類的接口轉換成客戶期望的另一個接口,使得原本由于接口不相容而不能一起工作的類可以一起工作。扳手提供了一個擴充卡的例子。一個孔套在棘齒上,棘齒的每個邊的尺寸是相同的。在美國典型的邊長為1/2''和1/4''。顯然,如果不使用一個擴充卡的話,1/2''的棘齒不能适合1/4''的孔。一個1/2''至1/4''的擴充卡具有一個1/2''的陰槽來套上一個1/2''的齒,同時有一個1/4的陽槽來卡入1/4''的扳手。
圖3使用扳手擴充卡例子的擴充卡對象圖
擴充卡模式解說
我們還是以日志記錄程式為例子說明Adapter模式。現在有這樣一個場景:假設我們在軟體開發中要使用一個第三方的日志記錄工具,該日志記錄工具支援資料庫日志記錄DatabaseLog和文本檔案記錄FileLog兩種方式,它提供給我們的API接口是Write()方法,使用方法如下:
Log.Write("Logging Message!");
當軟體系統開發進行到一半時,處于某種原因不能繼續使用該日志記錄工具了,需要采用另外一個日志記錄工具,它同樣也支援資料庫日志記錄DatabaseLog和文本檔案記錄FileLog兩種方式,隻不過它提供給我們的API接口是WriteLog()方法,使用方法如下:
Log.WriteLog("Logging Message!");
該日志記錄工具的類結構圖如下:
圖4日志記錄工具類結構圖
它的實作代碼如下:
public abstract class LogAdaptee
{
public abstract void WriteLog();
}
public class DatabaseLog:LogAdaptee
public override void WriteLog()
{
Console.WriteLine("Called WriteLog Method");
}
}
public class FileLog:LogAdaptee
在我們開發完成的應用程式中日志記錄接口中(不妨稱之為ILogTarget接口,在本例中為了更加清楚地說明,在命名上采用了Adapter模式中的相關角色名字),卻用到了大量的Write()方法,程式已經全部通過了測試,我們不能去修改該接口。代碼如下:
public interface ILogTarget
void Write();
這時也許我們會想到修改現在的日志記錄工具的API接口,但是由于版權等原因我們不能夠修改它的源代碼,此時Adapter模式便可以派上用場了。下面我們通過Adapter模式來使得該日志記錄工具能夠符合我們目前的需求。
前面說過,Adapter模式有兩種實作形式的實作結構,首先來看一下類擴充卡如何實作。現在唯一可行的辦法就是在程式中引入新的類型,讓它去繼承LogAdaptee類,同時又實作已有的ILogTarget接口。由于LogAdaptee有兩種類型的方式,自然我們要引入兩個分别為DatabaseLogAdapter和FileLogAdapter的類。
圖5 引入類擴充卡後的結構圖
實作代碼如下:
public class DatabaseLogAdapter:DatabaseLog,ILogTarget
public void Write()
WriteLog();
public class FileLogAdapter:FileLog,ILogTarget
this.WriteLog();
這裡需要注意的一點是我們為每一種日志記錄方式都編寫了它的适配類,那為什麼不能為抽象類LogAdaptee來編寫一個适配類呢?因為DatabaseLog和FileLog雖然同時繼承于抽象類LogAdaptee,但是它們具體的WriteLog()方法的實作是不同的。隻有繼承于該具體類,才能保留其原有的行為。
我們看一下這時用戶端的程式的調用方法:
public class App
public static void Main()
ILogTarget dbLog = new DatabaseLogAdapter();
dbLog.Write("Logging Database...");
ILogTarget fileLog = new FileLogAdapter();
fileLog.Write("Logging File...");
下面看一下如何通過對象擴充卡的方式來達到我們适配的目的。對象擴充卡是采用對象組合而不是使用繼承,類結構圖如下:
圖6引入對象擴充卡後的結構圖
public class LogAdapter:ILogTarget
private LogAdaptee _adaptee;
public LogAdapter(LogAdaptee adaptee)
this._adaptee = adaptee;
_adaptee.WriteLog();
與類擴充卡相比較,可以看到最大的差別是擴充卡類的數量減少了,不再需要為每一種具體的日志記錄方式來建立一個擴充卡類。同時可以看到,引入對象擴充卡後,擴充卡類不再依賴于具體的DatabaseLog類和FileLog類,更好的實作了松耦合。
再看一下用戶端程式的調用方法:
ILogTarget dbLog = new LogAdapter(new DatabaseLog());
ILogTarget fileLog = new LogAdapter(new FileLog());
fileLog.Write("Logging Database...");
通過Adapter模式,我們很好的實作了對現有元件的複用。對比以上兩種适配方式,可以總結出,在類适配方式中,我們得到的擴充卡類DatabaseLogAdapter和FileLogAdapter具有它所繼承的父類的所有的行為,同時也具有接口ILogTarget的所有行為,這樣其實是違背了面向對象設計原則中的類的單一職責原則,而對象擴充卡則更符合面向對象的精神,是以在實際應用中不太推薦類适配這種方式。再換個角度來看類适配方式,假設我們要适配出來的類在記錄日志時同時寫入檔案和資料庫,那麼用對象擴充卡我們會這樣去寫:
private LogAdaptee _adaptee1;
private LogAdaptee _adaptee2;
public LogAdapter(LogAdaptee adaptee1,LogAdaptee adaptee2)
this._adaptee1 = adaptee1;
this._adaptee2 = adaptee2;
_adaptee1.WriteLog();
_adaptee2.WriteLog();
如果改用類擴充卡,難道這樣去寫:
public class DatabaseLogAdapter:DatabaseLog,FileLog,ILogTarget
//WriteLog();
顯然是不對的,這樣的解釋雖說有些牽強,也足以說明一些問題,當然了并不是說類擴充卡在任何情況下都不使用,針對開發場景不同,某些時候還是可以用類擴充卡的方式。
.NET中的擴充卡模式
1.Adapter模式在.NET Framework中的一個最大的應用就是COM Interop。COM Interop就好像是COM和.NET之間的一條紐帶,一座橋梁。我們知道,COM元件對象與.NET類對象是完全不同的,但為了使COM客戶程式象調用COM元件一樣調用.NET對象,使.NET程式
象使用.NET對象一樣使用COM元件,微軟在處理方式上采用了Adapter模式,對COM對象進行包裝,這個包裝類就是RCW(Runtime Callable Wrapper)。RCW實際上是runtime生成的一個.NET類,它包裝了COM元件的方法,并内部實作對COM元件的調用。如下圖所示:
圖7 .NET程式與COM互相調用示意圖
2..NET中的另一個Adapter模式的應用就是DataAdapter。ADO.NET為統一的資料通路提供了多個接口和基類,其中最重要的接口之一是IdataAdapter。與之相對應的DataAdpter是一個抽象類,它是ADO.NET與具體資料庫操作之間的資料擴充卡的基類。DataAdpter起到了資料庫到DataSet橋接器的作用,使應用程式的資料操作統一到DataSet上,而與具體的資料庫類型無關。甚至可以針對特殊的資料源編制自己的DataAdpter,進而使我們的應用程式與這些特殊的資料源相相容。注意這是一個擴充卡的變體。
實作要點
1.Adapter模式主要應用于“希望複用一些現存的類,但是接口又與複用環境要求不一緻的情況”,在遺留代碼複用、類庫遷移等方面非常有用。
2.Adapter模式有對象擴充卡和類擴充卡兩種形式的實作結構,但是類擴充卡采用“多繼承”的實作方式,帶來了不良的高耦合,是以一般不推薦使用。對象擴充卡采用“對象組合”的方式,更符合松耦合精神。
3.Adapter模式的實作可以非常的靈活,不必拘泥于GOF23中定義的兩種結構。例如,完全可以将Adapter模式中的“現存對象”作為新的接口方法參數,來達到适配的目的。
4.Adapter模式本身要求我們盡可能地使用“面向接口的程式設計”風格,這樣才能在後期很友善的适配。[以上幾點引用自MSDN WebCast]
效果
對于類擴充卡:
1.用一個具體的Adapter類對Adaptee和Taget進行比對。結果是當我們想要比對一個類以及所有它的子類時,類Adapter将不能勝任工作。
2.使得Adapter可以重定義Adaptee的部分行為,因為Adapter是Adaptee的一個子類。
3.僅僅引入了一個對象,并不需要額外的指針一間接得到Adaptee.
對于對象擴充卡:
1.允許一個Adapter與多個Adaptee,即Adaptee本身以及它的所有子類(如果有子類的話)同時工作。Adapter也可以一次給所有的Adaptee添加功能。
2.使得重定義Adaptee的行為比較困難。這就需要生成Adaptee的子類并且使得Adapter引用這個子類而不是引用Adaptee本身。
适用性
在以下各種情況下使用擴充卡模式:
1.系統需要使用現有的類,而此類的接口不符合系統的需要。
2.想要建立一個可以重複使用的類,用于與一些彼此之間沒有太大關聯的一些類,包括一些可能在将來引進的類一起工作。這些源類不一定有很複雜的接口。
3.(對對象擴充卡而言)在設計裡,需要改變多個已有子類的接口,如果使用類的擴充卡模式,就要針對每一個子類做一個擴充卡,而這不太實際。
總結
總之,通過運用Adapter模式,就可以充分享受進行類庫遷移、類庫重用所帶來的樂趣。
參考資料
閻宏,《Java與模式》,電子工業出版社
James W. Cooper,《C#設計模式》,電子工業出版社
Alan Shalloway James R. Trott,《Design Patterns Explained》,中國電力出版社
MSDN WebCast 《C#面向對象設計模式縱橫談(7):Adapter 擴充卡模式(結構型模式)》
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。