從容自若的CTO
讓我們假設(shè)這樣一個場景:一年以前,Media公司開發(fā)出一套通過電腦接收廣播的Radio仿真軟件產(chǎn)品。(有這樣的產(chǎn)品嗎,能真正接收廣播的軟件?我表示懷疑)這個產(chǎn)品早已投入市場,客戶已經(jīng)在使用了。后來,Media公司將開發(fā)重心轉(zhuǎn)移到數(shù)字媒體上。于是他們投入了大量的人力物力,最后開發(fā)出了完美的媒體播放器軟件。這個播放器支持大多數(shù)媒體文件,包括音頻媒體和視頻媒體。該產(chǎn)品取得了成功,也得到了用戶的好評。
不過,現(xiàn)實生活中總有些刁鉆的客戶,比如說wayfarer,就是鄙人了,素愛懷舊。在使用媒體播放器的時候,想起了在初中的時候就使用的收錄機。磁帶、廣播,一機兩用,真是令人懷念。于是我向Media公司提出了建議,希望能在媒體播放器中增加收音的功能。Media的CEO對這個似乎有些嗤之以鼻??墒窍駑ayfarer這樣的用戶越來越多,呼聲也越來越高。為了產(chǎn)品的市場,為了公司的前景,這位CEO不得不慎重考慮這個需求了。當(dāng)首席執(zhí)行官就是好,趕緊把這個燙手山芋拋給了CTO。
卻看這位CTO仍然是從容不迫,臉上掛滿自信的微笑。CEO不解,問他何故如此從容?CTO淡然一笑,吐出一字真言:“Adapter”。
呵呵,笑話了。設(shè)計模式可不是什么Bible,也非神奇的魔咒。不過對于以上場景,使用Adapter卻是最佳的應(yīng)用!且請聽我慢慢道來。
已有產(chǎn)品:MediaPlayer、RadioPlayer;
分析:MediaPlayer是面向客戶的外觀,即表示層,它調(diào)用了對應(yīng)的業(yè)務(wù)層,該層實現(xiàn)了IMedia接口。同理RadioPlayer也是面向客戶的外觀,它調(diào)用的業(yè)務(wù)層,是收聽廣播的業(yè)務(wù),并實現(xiàn)了IRadio接口。
目的:將RadioPlayer的業(yè)務(wù)添加到MediaPlayer的外觀中。原有的RadioPlayer不再使用。
既然與MediaPlayer、RadioPlayer的業(yè)務(wù)有關(guān),所以我們有必要分析其各自的業(yè)務(wù)結(jié)構(gòu)。MediaPlayer業(yè)務(wù)層結(jié)構(gòu):

為了簡化,我這里將所有的方法都放在一個接口IMedia里(這個設(shè)計還有很多重構(gòu)的空間,我會在后續(xù)文章中繼續(xù)關(guān)注)。在本文的結(jié)構(gòu)中,視頻媒體和音頻媒體的方法是相同的,本來我可以令各媒體文件繼承同一個抽象類Media。但現(xiàn)實情況顯然不是這樣,所以我仍然保留這個系列文章中原有的結(jié)構(gòu)。以下是每個方法的說明:
Play():播放媒體文件;
Stop():停止播放;
Pause():暫停播放;
OpenFile():打開媒體文件;
CloseFile():關(guān)閉媒體文件;
Forward():前進播放文件;
Back():后退播放文件;
OK,我們再來看看RadioPlayer的業(yè)務(wù)層結(jié)構(gòu):

RadioPlayer的業(yè)務(wù)均抽象為IRadio接口。并由抽象類Radio實現(xiàn)該接口。FM為調(diào)頻收音,SW為短波收音。另外還有其他的,例如中波等,就不在詳細列出。各方法的功能說明如下:
Receive():接收廣播;
Stop():停止接收廣播;
TurnOn():打開收音;
TurnOff():關(guān)閉收音;
ChangeChannel(bool direction):切換頻率。參數(shù)direction為true時,則往上;否則往下。當(dāng)然也可以使用枚舉類型。
媒體播放器的業(yè)務(wù)由一個統(tǒng)一的Client類進行處理,它包括一系列的靜態(tài)方法以實現(xiàn)對原有媒體類型的調(diào)用:
public class Client
{
public static void Play(IMedia media)
{
media.Play();
}
public static void OpenFile(IMedia media)
{
media.OpenFile();
}
//……其他方法略;
}
MediaPlayer播放器本身,其外觀則是一個WinForm應(yīng)用程序,該應(yīng)用程序?qū)⒄{(diào)用Client的相關(guān)靜態(tài)方法。如:
Client.Play(new MP3());
現(xiàn)在看看我們需要實現(xiàn)的。我需要將RadioPlayer的業(yè)務(wù),即抽象為IRadio接口的對象,放到MediaPlayer中。糟糕的是,Client的各個方法傳遞的參數(shù)類型,為IMedia接口。怎么才能將實現(xiàn)IRadio接口的對象傳遞到Client的方法中呢?對了,這就是適配,就是為IRadio對象適配成符合IMedia接口行為的過程。打一個不好聽的比方,就好比一只狼,要讓自己鉆進羊群里,而不被發(fā)現(xiàn),就需要找一張羊皮來披上。俗語云:“披著羊皮的狼”是也。不過,我們要注意的是,狼雖然不是羊,但有著和羊相似的屬性。它和羊體形相似,照樣能跑,能吃,只是吃的不是草,而是肉而已。你總不能為一張桌子披上羊皮,去裝羊吧。而文中的IMedia類型和IRadio類型,還是有很多相似之處的。
現(xiàn)在,我們就為IRadio接口進行適當(dāng)?shù)陌b。由于這是兩個接口進行匹配的過程,所以我們通常名之為“適配”,而非“包裹”。那么它們之間有相似性嗎?有!
IMedia IRadio
Play() Receive()
Stop() Stop()
OpenFile() TurnOn()
CloseFile() TurnOff()
Forward() ChangeChannel(true)
Back() ChangeChannel(false)
當(dāng)然現(xiàn)實情況并非總是那么完美。可能IMedia的方法中,IRadio可能并不需要。沒關(guān)系,我們只提供該方法就可以了,方法的實現(xiàn)可以為空,如Pause()方法。也有可能IRadio的一些方法,IMedia并沒有,此時的Adaptor模式,就將被適配對象的接口變寬了,也就是說引入了新的行為,這就類似于我系列文章之二所描述的。
不管現(xiàn)實的某些情況是多么的不如意,但至少通過引入Adapter模式,我們就不需要改變原有的IMedia和IRadio的相關(guān)對象與業(yè)務(wù)了。要修改的,僅僅是客戶端,以及增加一個新的Adapter結(jié)構(gòu)而已。
分析結(jié)束,開始動手術(shù)吧。先看類的Adapter模式:

類圖好像很復(fù)雜,不過請大家主要關(guān)注橙色的兩個類FMAdapter和SWAdapter。FMAdapter類是FM類型的Adapter,它繼承了FM類,并實現(xiàn)了IMedia接口。通過這種方式,原有的FM類型的行為,就被適配為符合IMedia類型的新類型。代碼如下:
public class FMAdapter:FM,IMedia
{
public void Play()
{
this.Receive();
}
public void Forward()
{
this.ChangeChannel(true);
}
public void Pause(){}//Radio類型沒有該行為,令其為空,或引入異常機制;
//其他方法略
……
}
SWAdapter的實現(xiàn)方式完全相同,就不贅述。
由于新的Adapter類均實現(xiàn)了IMedia接口,因此,該類型的對象可以安全正確地作為Client靜態(tài)方法的參數(shù)對象傳入。從外部行為的表現(xiàn)來看,沒有區(qū)別。如:
Client.Play(new FMAdapter());
它調(diào)用了FMAdapter的Play方法,而其內(nèi)部,實質(zhì)上調(diào)用的是FM的Receiver()方法。
再看對象的Adapter模式,就更簡單了。

只需要一個Adapter類RadioAdapter,然后實現(xiàn)IMedia接口。沒有繼承關(guān)系了,而是聚合了Radio對象。注意,這里聚合的是抽象類對象Radio,而不是具體的FM或SW。
public class RadioAdapter:IMedia
{
private Radio _radio;
public RadioAdapter(Radio radio)
{
this._radio = radio;
}
public void Play()
{
_radio.Receive();
}
public void Forward()
{
_radio.ChangeChannel(true);
}
public void Pause(){}//Radio類型沒有該行為,另其為空,或引入異常機制;
//其他方法略
……
}
調(diào)用Client的靜態(tài)方法:
Client.Play(new RadioAdapter(new FM()));
通過引入Adapter模式,我們在不改變原有IMedia和IRadio的情況下,順利地將IRadio類型適配成了IMedia類型。此時,我們只需要在MediaPlayer的客戶端調(diào)用中加入原來RadioPlayer的業(yè)務(wù)即可,基本保證了原有系統(tǒng)的穩(wěn)定性。
上述實例,才真正體現(xiàn)了Adapter的價值(請大家一定注意區(qū)分本文實例需求,與系列之二實例需求的區(qū)別)。因此,我們可以得到兩個結(jié)論:
1、通過Adapter模式,為適配對象引入以前不具備的行為;此時建議使用類的Adapter模式。理由請參考:系列文章之二與之三;
2、將一個固有對象適配為另一種接口對象;這是Adapter模式最重要的功能。使用類的Adapter模式與對象的Adapter模式均可,但感覺使用對象的Adapter模式更簡單。
怎么樣,夠簡單吧?難怪我們的CTO如此從容,因為他已經(jīng)找到了終南捷徑!




