——.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
{
public
override
void
WriteLog()
{
Console
.
WriteLine(
"Called WriteLog Method"
);
}
}
在我們開發完成的應用程序中日志記錄接口中(不妨稱之為
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
{
public
void
Write()
{
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;
}
public
void
Write()
{
_adaptee
.
WriteLog();
}
}
與類適配器相比較,可以看到最大的區別是適配器類的數量減少了,不再需要為每一種具體的日志記錄方式來創建一個適配器類。同時可以看到,引入對象適配器后,適配器類不再依賴于具體的
DatabaseLog
類和
FileLog
類,更好的實現了松耦合。
再看一下客戶端程序的調用方法:
public
class
App
{
public
static
void
Main()
{
ILogTarget dbLog
=
new
LogAdapter(
new
DatabaseLog());
dbLog
.
Write(
"Logging Database..."
);
ILogTarget fileLog
=
new
LogAdapter(
new
FileLog());
fileLog
.
Write(
"Logging Database..."
);
}
}
通過
Adapter
模式,我們很好的實現了對現有組件的復用。對比以上兩種適配方式,可以總結出,在類適配方式中,我們得到的適配器類
DatabaseLogAdapter
和
FileLogAdapter
具有它所繼承的父類的所有的行為,同時也具有接口
ILogTarget
的所有行為,這樣其實是違背了面向對象設計原則中的類的單一職責原則,而對象適配器則更符合面向對象的精神,所以在實際應用中不太推薦類適配這種方式。再換個角度來看類適配方式,假設我們要適配出來的類在記錄日志時同時寫入文件和數據庫,那么用對象適配器我們會這樣去寫:
public
class
LogAdapter:ILogTarget
{
private
LogAdaptee _adaptee1;
private
LogAdaptee _adaptee2;
public
LogAdapter(LogAdaptee adaptee1,LogAdaptee adaptee2)
{
this
.
_adaptee1
=
adaptee1;
this
.
_adaptee2
=
adaptee2;
}
public
void
Write()
{
_adaptee1
.
WriteLog();
_adaptee2
.
WriteLog();
}
}
如果改用類適配器,難道這樣去寫:
public
class
DatabaseLogAdapter:DatabaseLog,FileLog,ILogTarget
{
public
void
Write()
{
//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
適配器模式
(
結構型模式
)
》
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
