從容自若的CTO
讓我們假設這樣一個場景:一年以前,Media公司開發出一套通過電腦接收廣播的Radio仿真軟件產品。(有這樣的產品嗎,能真正接收廣播的軟件?我表示懷疑)這個產品早已投入市場,客戶已經在使用了。后來,Media公司將開發重心轉移到數字媒體上。于是他們投入了大量的人力物力,最后開發出了完美的媒體播放器軟件。這個播放器支持大多數媒體文件,包括音頻媒體和視頻媒體。該產品取得了成功,也得到了用戶的好評。
不過,現實生活中總有些刁鉆的客戶,比如說wayfarer,就是鄙人了,素愛懷舊。在使用媒體播放器的時候,想起了在初中的時候就使用的收錄機。磁帶、廣播,一機兩用,真是令人懷念。于是我向Media公司提出了建議,希望能在媒體播放器中增加收音的功能。Media的CEO對這個似乎有些嗤之以鼻。可是像wayfarer這樣的用戶越來越多,呼聲也越來越高。為了產品的市場,為了公司的前景,這位CEO不得不慎重考慮這個需求了。當首席執行官就是好,趕緊把這個燙手山芋拋給了CTO。
卻看這位CTO仍然是從容不迫,臉上掛滿自信的微笑。CEO不解,問他何故如此從容?CTO淡然一笑,吐出一字真言:“Adapter”。
呵呵,笑話了。設計模式可不是什么Bible,也非神奇的魔咒。不過對于以上場景,使用Adapter卻是最佳的應用!且請聽我慢慢道來。
已有產品:MediaPlayer、RadioPlayer;
分析:MediaPlayer是面向客戶的外觀,即表示層,它調用了對應的業務層,該層實現了IMedia接口。同理RadioPlayer也是面向客戶的外觀,它調用的業務層,是收聽廣播的業務,并實現了IRadio接口。
目的:將RadioPlayer的業務添加到MediaPlayer的外觀中。原有的RadioPlayer不再使用。
既然與MediaPlayer、RadioPlayer的業務有關,所以我們有必要分析其各自的業務結構。MediaPlayer業務層結構:
為了簡化,我這里將所有的方法都放在一個接口IMedia里(這個設計還有很多重構的空間,我會在后續文章中繼續關注)。在本文的結構中,視頻媒體和音頻媒體的方法是相同的,本來我可以令各媒體文件繼承同一個抽象類Media。但現實情況顯然不是這樣,所以我仍然保留這個系列文章中原有的結構。以下是每個方法的說明:
Play():播放媒體文件;
Stop():停止播放;
Pause():暫停播放;
OpenFile():打開媒體文件;
CloseFile():關閉媒體文件;
Forward():前進播放文件;
Back():后退播放文件;
OK,我們再來看看RadioPlayer的業務層結構:
RadioPlayer的業務均抽象為IRadio接口。并由抽象類Radio實現該接口。FM為調頻收音,SW為短波收音。另外還有其他的,例如中波等,就不在詳細列出。各方法的功能說明如下:
Receive():接收廣播;
Stop():停止接收廣播;
TurnOn():打開收音;
TurnOff():關閉收音;
ChangeChannel(bool direction):切換頻率。參數direction為true時,則往上;否則往下。當然也可以使用枚舉類型。
媒體播放器的業務由一個統一的Client類進行處理,它包括一系列的靜態方法以實現對原有媒體類型的調用:
public class Client
{
public static void Play(IMedia media)
{
media.Play();
}
public static void OpenFile(IMedia media)
{
media.OpenFile();
}
//……其他方法略;
}
MediaPlayer播放器本身,其外觀則是一個WinForm應用程序,該應用程序將調用Client的相關靜態方法。如:
Client.Play(new MP3());
現在看看我們需要實現的。我需要將RadioPlayer的業務,即抽象為IRadio接口的對象,放到MediaPlayer中。糟糕的是,Client的各個方法傳遞的參數類型,為IMedia接口。怎么才能將實現IRadio接口的對象傳遞到Client的方法中呢?對了,這就是適配,就是為IRadio對象適配成符合IMedia接口行為的過程。打一個不好聽的比方,就好比一只狼,要讓自己鉆進羊群里,而不被發現,就需要找一張羊皮來披上。俗語云:“披著羊皮的狼”是也。不過,我們要注意的是,狼雖然不是羊,但有著和羊相似的屬性。它和羊體形相似,照樣能跑,能吃,只是吃的不是草,而是肉而已。你總不能為一張桌子披上羊皮,去裝羊吧。而文中的IMedia類型和IRadio類型,還是有很多相似之處的。
現在,我們就為IRadio接口進行適當的包裝。由于這是兩個接口進行匹配的過程,所以我們通常名之為“適配”,而非“包裹”。那么它們之間有相似性嗎?有!
IMedia IRadio
Play() Receive()
Stop() Stop()
OpenFile() TurnOn()
CloseFile() TurnOff()
Forward() ChangeChannel(true)
Back() ChangeChannel(false)
當然現實情況并非總是那么完美。可能IMedia的方法中,IRadio可能并不需要。沒關系,我們只提供該方法就可以了,方法的實現可以為空,如Pause()方法。也有可能IRadio的一些方法,IMedia并沒有,此時的Adaptor模式,就將被適配對象的接口變寬了,也就是說引入了新的行為,這就類似于我系列文章之二所描述的。
不管現實的某些情況是多么的不如意,但至少通過引入Adapter模式,我們就不需要改變原有的IMedia和IRadio的相關對象與業務了。要修改的,僅僅是客戶端,以及增加一個新的Adapter結構而已。
分析結束,開始動手術吧。先看類的Adapter模式:
類圖好像很復雜,不過請大家主要關注橙色的兩個類FMAdapter和SWAdapter。FMAdapter類是FM類型的Adapter,它繼承了FM類,并實現了IMedia接口。通過這種方式,原有的FM類型的行為,就被適配為符合IMedia類型的新類型。代碼如下:
public class FMAdapter:FM,IMedia
{
public void Play()
{
this.Receive();
}
public void Forward()
{
this.ChangeChannel(true);
}
public void Pause(){}//Radio類型沒有該行為,令其為空,或引入異常機制;
//其他方法略
……
}
SWAdapter的實現方式完全相同,就不贅述。
由于新的Adapter類均實現了IMedia接口,因此,該類型的對象可以安全正確地作為Client靜態方法的參數對象傳入。從外部行為的表現來看,沒有區別。如:
Client.Play(new FMAdapter());
它調用了FMAdapter的Play方法,而其內部,實質上調用的是FM的Receiver()方法。
再看對象的Adapter模式,就更簡單了。
只需要一個Adapter類RadioAdapter,然后實現IMedia接口。沒有繼承關系了,而是聚合了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類型沒有該行為,另其為空,或引入異常機制;
//其他方法略
……
}
調用Client的靜態方法:
Client.Play(new RadioAdapter(new FM()));
通過引入Adapter模式,我們在不改變原有IMedia和IRadio的情況下,順利地將IRadio類型適配成了IMedia類型。此時,我們只需要在MediaPlayer的客戶端調用中加入原來RadioPlayer的業務即可,基本保證了原有系統的穩定性。
上述實例,才真正體現了Adapter的價值(請大家一定注意區分本文實例需求,與系列之二實例需求的區別)。因此,我們可以得到兩個結論:
1、通過Adapter模式,為適配對象引入以前不具備的行為;此時建議使用類的Adapter模式。理由請參考:系列文章之二與之三;
2、將一個固有對象適配為另一種接口對象;這是Adapter模式最重要的功能。使用類的Adapter模式與對象的Adapter模式均可,但感覺使用對象的Adapter模式更簡單。
怎么樣,夠簡單吧?難怪我們的CTO如此從容,因為他已經找到了終南捷徑!