|
前文說(shuō)到一位用戶拿著業(yè)界標(biāo)準(zhǔn)開(kāi)關(guān)(一個(gè)標(biāo)準(zhǔn)的StandardSwitcher,它依賴IStandardSwitchable接口才能工作,然而目前我們的燈并不支持這個(gè)接口)出現(xiàn)在我面前,叫囂著他的“標(biāo)準(zhǔn)開(kāi)關(guān)”應(yīng)該能打開(kāi)我們的燈。好吧,這個(gè)需求是合理的,的確應(yīng)該支持。但是該死的是,為什么沒(méi)有早一點(diǎn)兒知道這個(gè)標(biāo)準(zhǔn)的存在呢?這樣就不需要花費(fèi)時(shí)間和人力定義這個(gè)接口,現(xiàn)在也不會(huì)這么糾結(jié)。和上次一樣,先講故事、演進(jìn)方案,再分析背后的思想。 這回主要講解Adapter模式,GoF講解了這個(gè)模式是什么,怎么用,用在什么地方。我想來(lái)解釋一下Adapter模式的要點(diǎn)是什么,對(duì)Adapter模式的延展,以及對(duì)Adapter模式的誤用。順便得瑟一下我對(duì)面向?qū)ο笤O(shè)計(jì)的理解。 兩個(gè)方案 現(xiàn)在有兩個(gè)選擇。
第一個(gè)方案很簡(jiǎn)單,就是讓Light多實(shí)現(xiàn)個(gè)接口就OK了。圖就不給了。 現(xiàn)在分析第二個(gè)方案,標(biāo)準(zhǔn)接口依賴IStandardSwitchable接口,那我們必須有一個(gè)類來(lái)實(shí)現(xiàn)它,并完成所需要的功能——操作燈。咱也是學(xué)過(guò)設(shè)計(jì)模式的人,這個(gè)問(wèn)題很明顯可以用Adapter模式來(lái)解釋。 相關(guān)類圖很容易就可以畫出來(lái)。 圖1 讓燈支持IStandardSwitchable接口的方案 其對(duì)應(yīng)的代碼會(huì)是這個(gè)樣子:
代碼1 Job Done。Light通過(guò)SwitcherAdapter支持了新的接口,這簡(jiǎn)直就是應(yīng)用適配器模式的典范啊。(嗯,這句的確是反話,不過(guò)你猜出來(lái)為什么這個(gè)Adapter不屬于適配器模式嗎?) “上回真是白跟你說(shuō)了那么多,平時(shí)沒(méi)覺(jué)得你這么不開(kāi)竅啊。你自己好好想想吧!”背后看著我畫UML圖的設(shè)計(jì)Guru好像有點(diǎn)兒生氣。 上回?我冷靜下來(lái)回想上回的內(nèi)容和現(xiàn)在的問(wèn)題。上回講的DIP,講不要依賴實(shí)現(xiàn),要依賴抽象。再想想目前的需求,我們有燈,有收音機(jī),如果用戶說(shuō)要用標(biāo)準(zhǔn)開(kāi)關(guān)開(kāi)收音機(jī),難道還要實(shí)現(xiàn)一個(gè)RadioAdapter不成?這顯然違反了OCP。 需求是要“通過(guò)加一層讓燈支持標(biāo)準(zhǔn)開(kāi)關(guān)”,但是并不是說(shuō)這一層就要使用燈,為了讓這個(gè)Adapter更加通用,應(yīng)該讓Adapter依賴ISwitchable接口。像下面這個(gè)樣子。 圖2 Adapter模式 與代碼1的差別,僅僅是SwitcherAdapter里的Switchee屬性的類型改成ISwitchable而已。代碼就不再貼了。其所體現(xiàn)的原則就是上一篇講的DIP。 這個(gè)事兒其實(shí)任何人靜靜地想想都能想到。但我繞這個(gè)彎子,其實(shí)是想順便表達(dá)這樣一個(gè)意思:一個(gè)緊急需求來(lái)了的時(shí)候,人們更容易傾向于把完成工作放在第一位,從而一時(shí)忽視了設(shè)計(jì)的嚴(yán)謹(jǐn)度,事后又忘了重構(gòu),于是Bad Smell就這樣產(chǎn)生了。當(dāng)然,這些大家也都知道。 面向?qū)ο蟮脑O(shè)計(jì)并不是對(duì)現(xiàn)實(shí)的模擬 (這一節(jié)算是一個(gè)插曲吧,因?yàn)檫@個(gè)論點(diǎn)太大,寫出來(lái)都覺(jué)得不自量力,不寫又覺(jué)得對(duì)不起自己愛(ài)得瑟的作風(fēng)。一點(diǎn)拙見(jiàn),大家多多批評(píng)。覺(jué)得偏題太遠(yuǎn)的話可以直接看一下節(jié)。) 但是(重點(diǎn)來(lái)了),為什么緊張時(shí)做出的直觀設(shè)計(jì)更可能是錯(cuò)誤的呢?因?yàn)槿艘痪o張就容易憑感覺(jué),而使用直覺(jué)做設(shè)計(jì)時(shí),大都會(huì)以現(xiàn)實(shí)世界為原本,但是良好的面向?qū)ο笤O(shè)計(jì),是絕對(duì)不能僅僅依靠現(xiàn)實(shí)世界的。其實(shí)圖1 的設(shè)計(jì)從直覺(jué)上來(lái)講是符合需求,也很符合人們對(duì)這個(gè)世界的認(rèn)知的。但是它并不是一個(gè)良好的面向?qū)ο笤O(shè)計(jì)。圖2是相對(duì)良好的設(shè)計(jì),但是圖2顯然又沒(méi)有圖1 那么直觀,那么好理解,那么符合這個(gè)世界的真實(shí)狀態(tài)。 圖2和圖1 的差別僅僅在于Adapter要依賴誰(shuí)上,Adapter要依賴于ISwitchable接口這個(gè)事兒,并不是為了更真實(shí)地模擬這個(gè)世界,而純粹地是為了解耦合而出現(xiàn)(或者說(shuō),為了依賴抽象)。但是在現(xiàn)實(shí)世界中,是不存在解不解耦合的概念的。解耦合是為了保證設(shè)計(jì)上的靈活性引入的概念。 現(xiàn)實(shí)中事物間的依賴都是具體的,是為了復(fù)用、靈活性等才引入的抽象,客觀現(xiàn)實(shí)是不存在抽象的。抽象是要取決于你是如何看待客觀事物的。舉個(gè)例子,在動(dòng)物學(xué)家看來(lái),人與動(dòng)物間有IS-A關(guān)系;但是如果你是要開(kāi)發(fā)一款MMORPG游戲,人(NPC和Avatar)和動(dòng)物(一般會(huì)是怪物)應(yīng)該是不會(huì)有IS-A關(guān)系的。觀察的角度不同,就會(huì)得出不同的設(shè)計(jì);這些設(shè)計(jì)沒(méi)有對(duì)錯(cuò)之分,只有是否滿足需求之別。 所以,有些地方,把面向?qū)ο蟮脑O(shè)計(jì)過(guò)程解釋為對(duì)現(xiàn)實(shí)世界的模擬是很片面的。如果僅僅以現(xiàn)實(shí)世界的樣子對(duì)系統(tǒng)進(jìn)行設(shè)計(jì),得出的設(shè)計(jì)很可能是僵化的,就像圖1那樣。(有人可能想說(shuō)我曲解了人家的意思,但是我想說(shuō),你寫成那個(gè)樣子明明就是故意給人誤解的,至少是很容易引導(dǎo)人誤解。容易被誤解,就是有問(wèn)題。沒(méi)什么好狡辯的。) 但是,這并不意味著做設(shè)計(jì)就要全面地抽象,模擬現(xiàn)實(shí)世界的好處是代碼容易理解,但是如果全部抽象成圖2那樣,所有都抽象出個(gè)接口,所有都依賴抽象,那代碼的可讀性顯然會(huì)下降。所以,好的面向?qū)ο笤O(shè)計(jì),會(huì)是真實(shí)地模擬現(xiàn)實(shí)與抽象現(xiàn)實(shí)間的取舍的過(guò)程。如果你看過(guò)一些功能相似、但實(shí)現(xiàn)不同的開(kāi)源框架,會(huì)發(fā)現(xiàn)有些好理解,有些不好理解,其根本原因就是其抽象的層次或者說(shuō)抽象的程度不一樣。抽象度過(guò)高,靈活性也許上去了,但是并不見(jiàn)得就是好事兒。過(guò)度設(shè)計(jì),就是因?yàn)閷?duì)現(xiàn)實(shí)的抽象度太高,造成可讀性差,不好維護(hù),還沒(méi)解決問(wèn)題,就先被問(wèn)題解決掉了。 上面的例子可能依然沒(méi)有什么說(shuō)服力。我再舉一個(gè)。上篇文章有人回復(fù)說(shuō), “開(kāi)關(guān)里面還包含一個(gè)開(kāi)關(guān)接口 ,很奇怪的方式。 在我看來(lái)應(yīng)該是燈光有開(kāi)關(guān)”。 我想感謝一下這位朋友,因?yàn)樗岢龅倪@個(gè)思路,我一開(kāi)始就潛意識(shí)地?zé)o視掉了。經(jīng)他一提,我才意識(shí)這也是設(shè)計(jì)過(guò)程中一類常見(jiàn)的問(wèn)題。這個(gè)設(shè)計(jì)是一個(gè)很真實(shí)地反應(yīng)現(xiàn)實(shí)的設(shè)計(jì)。但是并不是一個(gè)可行的類設(shè)計(jì)。如果你按這個(gè)方案寫代碼,就會(huì)發(fā)現(xiàn)很多問(wèn)題。原因我已經(jīng)回復(fù)了。 總結(jié)一下,做面向?qū)ο笤O(shè)計(jì)的時(shí)候,請(qǐng)記得自己要做的是什么?不要讓現(xiàn)實(shí)世界的“真實(shí)”的樣子混淆了視聽(tīng)。面向?qū)ο笤O(shè)計(jì),是以可復(fù)用地、靈活地實(shí)現(xiàn)需求為目標(biāo)的,對(duì)現(xiàn)實(shí)的抽象,而不是對(duì)現(xiàn)實(shí)的模擬;抽象的結(jié)果很可能在現(xiàn)實(shí)中并不存在。 Adapter模式的關(guān)鍵 Adapter模式最關(guān)鍵的要求是:Adapter是對(duì)兩個(gè)功能相近的接口間的適配。如果被適配的對(duì)象是個(gè)具體類,那么多數(shù)情況下,Adapter非但不會(huì)帶來(lái)好處,反而是僅僅增加了維護(hù)成本,就像前面說(shuō)的,有一個(gè)新的具體類出現(xiàn),就要同時(shí)添加一個(gè)Adapter。 (如果你非說(shuō)你見(jiàn)過(guò)很多 “適配”具體類的,你是對(duì)的,但是那叫Proxy,不叫Adapter,解決的也不是同一種問(wèn)題,而且多數(shù)情況下,Proxy是可以自動(dòng)生成的,所以不需要擔(dān)心加一個(gè)類,就要自己實(shí)現(xiàn)一個(gè)對(duì)應(yīng)的Proxy的問(wèn)題??梢杂孟旅孢@個(gè)圖對(duì)比一下,來(lái)自《敏捷軟件開(kāi)發(fā)》) 圖3 Proxy模式 這不是在死摳Adapter模式的含義。因?yàn)橹挥欣斫釧dapter的目標(biāo)、適用范圍之后,才不會(huì)誤用這個(gè)模式。見(jiàn)過(guò)不少人理解力很好或是英文很好,看到 Adapter這個(gè)詞是個(gè)模式就想當(dāng)然地覺(jué)得自己“知道”了這個(gè)模式的用法(畢竟這個(gè)模式也的確不復(fù)雜),并“用”了起來(lái)。比如圖1的那個(gè)例子,就是最常見(jiàn)的誤用之一。 這也不是在死摳名詞。給模式命名的好處之一就是讓兩個(gè)都懂模式的人溝通起來(lái)更順暢。模式名所表達(dá)的,不是一個(gè)簡(jiǎn)單的類關(guān)系圖,而是對(duì)要解決問(wèn)題的類型的定位和解決問(wèn)題的策略。 Adapter,表示遇到的問(wèn)題是接口不匹配。 Proxy,表示遇到的問(wèn)題是主體邏輯與附加邏輯(持久化、網(wǎng)絡(luò)傳輸?shù)龋┘m纏。 名詞用錯(cuò)了,就可能會(huì)帶來(lái)不必要的誤會(huì)。 如果你就是覺(jué)得沒(méi)必要死摳概念,下面的“廣義Adapter模式”可能會(huì)比較適合你。 廣義Adapter模式 這年頭好像什么東西都非要搞出個(gè)狹義和廣義之分。我個(gè)人比較反感這一點(diǎn),因?yàn)楠M廣之分的存在,本身就是一種對(duì)概念的模糊。這導(dǎo)致人們?cè)跍贤〞r(shí),如果遇到問(wèn)題,常常要想一下對(duì)方說(shuō)的是廣義的還是狹義的,而不是把焦點(diǎn)放在問(wèn)題本身。這像是給自己和對(duì)方找借口或是后路?;蛟S是因?yàn)榇蠹叶枷虢o自己留個(gè)后路,這東西才會(huì)這么流行。附經(jīng)典對(duì)白一則: “嗯?不對(duì)吧,不應(yīng)該是XXXX嗎?” “呃,我說(shuō)的是一種廣義上的XXXX?!?/i> “哦。(Shit!)” 每個(gè)人們學(xué)習(xí)模式,總會(huì)有自己的理解,自己的抽象。當(dāng)理解的角度不同的時(shí)候,就會(huì)把Adapter模式的內(nèi)涵延展到不同的地方。這就導(dǎo)致了不同人對(duì)廣義Adapter的定義是不同的。 比如《敏捷軟件開(kāi)發(fā)》,從邏輯關(guān)系出發(fā),把Adapter的概念延展為:使用一個(gè)特定的類,實(shí)現(xiàn)對(duì)方法調(diào)用的定向派發(fā)(我自己總結(jié)的,原文沒(méi)這話)。從這個(gè)概念上講,Adapter模式可以用于對(duì)具體類的適配。因?yàn)檫@個(gè)延展的概念實(shí)際上已經(jīng)超出了原有的GoF的定義。這顯然不能說(shuō)是錯(cuò)誤的,你甚至?xí)X(jué)得這個(gè)人水平真高,能對(duì)設(shè)計(jì)模式進(jìn)行再抽象,再擴(kuò)展。 但是問(wèn)題是,不同人對(duì)同一概念的延展方向是不同的。你覺(jué)得Adapter和Delegate/Event有什么相似之處嗎?我相信更多人會(huì)覺(jué)得Observer模式與Delegate/Event的相似之處更多些。因?yàn)闊o(wú)數(shù)的人和書都說(shuō)過(guò)C#的Delegate /Event機(jī)制就是Observer模式的一種具體實(shí)現(xiàn)。如果你面試的時(shí)候說(shuō),Delegation就是一種Adapter,你的面試可能就直接 Pass了。這事兒也的確真實(shí)地發(fā)生過(guò)。 但是如果去看《Pro Objective-C Design Pattern for iOS》第112頁(yè),對(duì)Adapter的描述真的是這樣的。 “The Delegation pattern was once one of the inspirations for cataloging the Adapter pattern in the “Gang of Four” book.” 如果你怕我斷章取義,可以自己去看。 這個(gè)人是從類與類之間的關(guān)系出發(fā),把具有相似結(jié)構(gòu)、交互方式的類的組合都定義為Adapter。你說(shuō)他的理解錯(cuò)了嗎?我只能說(shuō):“狹義來(lái)講,是錯(cuò)的,廣義來(lái)講,是對(duì)的?!钡@是這個(gè)世界上最操蛋的答案之一。 像上面鏈接的博客里描述的那個(gè)面試者,顯然就成了廣義與狹義之分的犧牲品——他說(shuō)的是廣義的Adapter,但是面試官想聽(tīng)到的是狹義的Adapter。(不過(guò)從后面的敘述來(lái)看,那個(gè)面試官也是半瓶子醋,問(wèn)Delegate的時(shí)候居然會(huì)順便問(wèn)異步,讓我不得不懷疑他是不是認(rèn)為事件是異步觸發(fā)的。) 對(duì)Adapter有獨(dú)特的理解很好,能把Adapter, Observer, Delegation, Proxy全統(tǒng)一起來(lái)理解更是NB。但是,其實(shí)在多數(shù)情況下,越是獨(dú)到的見(jiàn)解,越可能會(huì)給面對(duì)面的溝通帶來(lái)障礙。這些獨(dú)到的見(jiàn)解在個(gè)人頓悟模式的過(guò)程中很有用,寫到書里也很好,畢竟讀者可以細(xì)細(xì)體味,幫助讀者從不同的角度思考問(wèn)題;但想在面試之類的當(dāng)面溝通的場(chǎng)合上裝逼,然后自己的口才又不咋地。怕只會(huì)畫虎不成反類犬。 對(duì)Adapter模式的誤用 學(xué)歷史的時(shí)候,常常見(jiàn)到“左派”、“右派”這樣的詞,意思是他們走的路線不對(duì)。這個(gè)詞用得很形象,都是走極端。 模式的誤用,常見(jiàn)的誤用之一也是走極端。 圖2 的Adapter模式,成功的把標(biāo)準(zhǔn)的開(kāi)關(guān)接口適配到了我們的接口上。于是便有了一個(gè)順理成章的思路,ISwitchable和 IStandardSwitchable接口都是對(duì)開(kāi)關(guān)的定義,我們通過(guò)Adapter模式,讓支持IStandardSwitchable的開(kāi)關(guān)能夠開(kāi)我們的燈。 那么我們之前的這個(gè)設(shè)計(jì): 圖4. 第一回中提出的開(kāi)關(guān)開(kāi)燈方案(Abstract Server)
是不是應(yīng)該改成這樣? 圖5. 試圖把Adapter模式用于實(shí)現(xiàn)DIP 這個(gè)設(shè)計(jì)相比原來(lái)的設(shè)計(jì)方案,抽象度更高、耦合性更低,Light甚至不需要依賴ISwitchable接口就可以工作,這樣我們可以很有信心地說(shuō),我們可以讓一切類都支持ISwitchable接口! 這個(gè)想法很豐滿,但是現(xiàn)實(shí)很骨感。如果你認(rèn)真看過(guò)了前面的內(nèi)容,應(yīng)該已經(jīng)知道這個(gè)方案其實(shí)很爛的原因了。 這個(gè)世界很微妙,《敏捷軟件開(kāi)發(fā)》(P370)的確就把圖5稱為Adapter模式,不過(guò)你應(yīng)該懂的,他說(shuō)的是廣義的Adapter模式。并不是說(shuō)對(duì)具體類的Adapter就一定是誤用,如果沒(méi)有違反OCP就不是誤用,如果那個(gè)Light是個(gè)Utility類,就不算是誤用。 (如果你想噴Adapter模式本來(lái)就有兩種,一種是基于類的,一種是基于對(duì)象的,你最好先去把Adapter概念回個(gè)爐,我們說(shuō)的根本不是一碼事兒。) 誤用的原因 我自己總結(jié)了一下出現(xiàn)這種誤用的原因有三(這些原因會(huì)讓人出現(xiàn)各種形式的誤用,而不針對(duì)Adapter模式):
下回預(yù)告 我們的燈賣得好,用戶就多了起來(lái),需求也多了起來(lái)。這樣一下子來(lái)了兩個(gè)用戶,一個(gè)要求,我要用兩個(gè)開(kāi)關(guān)控制同一個(gè)燈(床頭一個(gè),走廊一個(gè),看來(lái)這用戶晚上常起夜);另一個(gè)要求,我想用一個(gè)開(kāi)關(guān)控制屋子里所有的燈(看來(lái)這用戶不差錢)。 那么,我們又需要做出怎樣的設(shè)計(jì)來(lái)應(yīng)對(duì)這些需求呢 |
|
|