新聞中心
開燈的例子

選開燈做例子,是因?yàn)檫@個(gè)例子既常見又簡單,而且潛在的需求多樣。對于最簡單的燈,從功能上講,按下燈上的開關(guān),燈就開了。
用代碼實(shí)現(xiàn)這樣一個(gè)有開關(guān)功能的燈,也是一件很容易的事情。
- public class Light
- {
- public void TurnOn() { Console.WriteLine("Light Turn On"); }
- public void TurnOff() { Console.WriteLine("Light Turn Off"); }
- }
代碼1
一個(gè)具有開關(guān)功能的燈就完成了。這個(gè)燈,功能完備、也滿足當(dāng)下的需求。一切美好。
直到有一天,有個(gè)客戶說,燈上的開關(guān)壞了,能不能換一個(gè)?我才意識到這個(gè)燈的設(shè)計(jì)有問題——它的開關(guān)是換不了的。一面給用戶解釋,一面考慮著把燈和開關(guān)分開。
咱也是學(xué)過設(shè)計(jì)模式的人,知道要面向接口編程,絕不應(yīng)該簡單地把Light類拆解成Light和Switcher兩個(gè)類。因?yàn)镾witcher不應(yīng)該依賴于具體實(shí)現(xiàn),于是寫出了下面的代碼。
- namespace Me.Lighting
- {
- public interface ILightable
- {
- void ShowLight();
- void HideLight();
- }
- public class Light : ILightable
- {
- public void ShowLight() { Console.WriteLine("Light Turn On"); }
- public void HideLight() { Console.WriteLine("Light Turn Off"); }
- }
- }
- namespace Me.Switch
- {
- using Me.Lighting;
- public class Switcher
- {
- public ILightable Light { get; set; }
- public void TurnOn() { Light.ShowLight(); }
- public void TurnOff() { Light.HideLight(); }
- }
- }
代碼 2
這個(gè)設(shè)計(jì),不僅分離了燈和開關(guān),甚至可以讓這個(gè)開關(guān)靈活地控制要開關(guān)哪個(gè)燈。只要在開關(guān)前設(shè)置一下就可以,多方便。我自信滿滿地遷入了代碼。
事實(shí)也證明這樣的設(shè)計(jì)是成功的,產(chǎn)品的靈活設(shè)計(jì)得到了用戶的認(rèn)可,銷量直線上升。
親,請看下代碼,在不使用什么別的設(shè)計(jì)模式的前提下,您覺得代碼2有什么問題?無論是什么角度的都可以(當(dāng)然,可能您的角度不是本文討論的重點(diǎn)),最好先回復(fù)下留個(gè)底,別事后諸葛。
如果您一眼看到了問題,請直接閱讀DIP那一節(jié)。
暗流涌動(dòng)
公司壯大之后 ,開始考慮向收音機(jī)行業(yè)進(jìn)軍。而且公司希望,這種靈活的設(shè)計(jì)可以沿用下去,收音機(jī)和燈的開關(guān)應(yīng)該可以通用,對用戶而言,都是撥那么一下。
我聽到這個(gè)信息也是相當(dāng)興奮,但是當(dāng)我開始著手寫代碼時(shí),發(fā)現(xiàn)一些壞味道,開關(guān)依賴于ILightable 接口,那么我的收音機(jī)不得不寫成這個(gè)樣子才能與現(xiàn)有的開關(guān)兼容。
- public class Radio : ILightable
- {
- public void ShowLight() { Console.WriteLine("Play radio"); }
- public void HideLight() { Console.WriteLine("Stop radio"); }
- }
代碼3
雖然可以工作,但是這是嚴(yán)重的壞味道。因?yàn)槿绻幸惶?,燈的接口變化,我卻要連收音機(jī)的代碼一起改。這種情況絕不應(yīng)該出現(xiàn)。且不用把LSP(Liskov替換原則)搬出來說教,很顯然Radio其實(shí)并沒有完成ILightable所定義的功能——發(fā)光。無論從哪個(gè)角度講都是錯(cuò)的。
一個(gè)可行的設(shè)計(jì)是,讓開關(guān)支持收音機(jī)的開啟和停止。像下面這樣。
- namespace Me.Radio
- {
- public interface IRadio
- {
- void Play();
- void Stop();
- }
- public class Radio : IRadio
- {
- public void Play() { Console.WriteLine("Play radio"); }
- public void Stop() { Console.WriteLine("Stop radio"); }
- }
- }
- namespace Me.Switch
- {
- using Me.Lighting;
- using Me.Radio;
- public class Switcher
- {
- public ILightable Light { get; set; }
- public IRadio Radio { get; set; }
- public void TurnOn()
- {
- if (Light != null) Light.ShowLight();
- else if (Radio != null) Radio.Play();
- }
- public void TurnOff() { Light.HideLight(); }
- }
- }
代碼4
我看來看去都覺得這個(gè)代碼太惡心了,因?yàn)镾witcher的實(shí)現(xiàn)方式違反了OCP(開放—封閉原則),如果這樣發(fā)展下去,公司的產(chǎn)品越豐富,這坨代碼就越難以維護(hù)。我的末日也就越近。
于是我的考慮Switcher的設(shè)計(jì)是不是有問題,我已經(jīng)用上面向接口編程了,為什么還是有問題呢?
Guru眼中的依賴
我把代碼發(fā)給了我的導(dǎo)師,一個(gè)設(shè)計(jì)Guru,他看完之后哭笑著說,你的基本功很扎實(shí),理論知識也很全面,可惜卻缺乏一定的經(jīng)驗(yàn)。面向接口編程沒有錯(cuò),但是更重要的是模型的建立。
簡單而言,你的開關(guān)的依賴關(guān)系錯(cuò)了。問你一個(gè)問題你就明白了,開關(guān)為什么要依賴ILightable呢?但是好在你有一定的設(shè)計(jì)基礎(chǔ),知道要提取出一個(gè)接口,所以要改成正確的設(shè)計(jì)也非常容易。你只需要把ILightable這個(gè)接口的名字改成ISwitchable,再把接口方法名字改下,并把它與Switcher放一起就行了。
聽罷,我恍然大悟。原來接口的名字和位置,也會給使用者帶來如此大的困擾。在先進(jìn)的開發(fā)工具的幫助下,瞬間就完成了這個(gè)簡單的重命名和移動(dòng)操作?,F(xiàn)在的代碼像這個(gè)樣子了。
- namespace Me.Lighting
- {
- using Me.Switch;
- public class Light : ISwitchable
- {
- public void TurnOn() { Console.WriteLine("Light Turn On"); }
- public void TurnOff() { Console.WriteLine("Light Turn Off"); }
- }
- }
- namespace Me.Radio
- {
- using Me.Switch;
- public class Radio : ISwitchable
- {
- public void TurnOn() { Console.WriteLine("Play radio"); }
- public void TurnOff() { Console.WriteLine("Stop radio"); }
- }
- }
- namespace Me.Switch
- {
- public interface ISwitchable
- {
- void TurnOn();
- void TurnOff();
- }
- public class Switcher
- {
- public ISwitchable Switchee { get; set; }
- public void TurnOn() { Switchee.TurnOn(); }
- public void TurnOff() { Switchee.TurnOff(); }
- }
- }
代碼5
注意:這個(gè)代碼與之前有問題的代碼2,只是各種名稱上的變化。結(jié)構(gòu)上一點(diǎn)兒沒變。
以后有新的產(chǎn)品,也只需要實(shí)現(xiàn)ISwitchable接口,就可以支持這個(gè)開關(guān)了。之前的失敗設(shè)計(jì),看似與這個(gè)設(shè)計(jì)相差無幾,但是其中蘊(yùn)含的設(shè)計(jì)思想天差地遠(yuǎn),也正是在這種地方,才更能體現(xiàn)出設(shè)計(jì)師間的差距。這一種設(shè)計(jì)所體現(xiàn)的,即是DIP(依賴倒置原則),的表現(xiàn)之一,接口應(yīng)當(dāng)被其使用者所擁有,而非其實(shí)現(xiàn)者。1
DIP(依賴倒置原則)
具體問題解決了,還需要把整個(gè)問題抽象一下,從本質(zhì)上了解一下DIP的含義。(我會盡量清楚,可能會有些啰嗦,但這比在回復(fù)里爭論要舒坦得多。)
假設(shè)有如下所示的類圖。假設(shè)我們要把這種關(guān)系解耦合。
圖1
注:圖1中的User表示使用者(調(diào)用者),而不是用戶的意思。
為什么要解耦合?
我說“假設(shè)要解耦合”,是因?yàn)樵趪L試解耦這種依賴關(guān)系之前,應(yīng)該先確定有沒有解耦的必要。這種關(guān)系在代碼中比比皆是,如果把所有的依賴都解耦,不僅工作量大、帶不來任何好處,而且引入了不必要的復(fù)雜度,最終演變成了過度設(shè)計(jì),增加了編碼成本和維護(hù)成本。(我已經(jīng)被人罵怕了,怕不說清楚這一點(diǎn),總要有人跳出來說我濫用模式,說這種關(guān)系要不要解耦要看情況,云云。都是好意,我也心領(lǐng)了,謝謝。但被人假設(shè)狗屁不通,總不太舒服。)
明確某個(gè)依賴關(guān)系是否需要被分解,是一件很復(fù)雜的事情,個(gè)人覺得并沒有什么準(zhǔn)則能讓你輕松地做出這個(gè)判斷。因?yàn)閹缀跛械囊蕾?,在一句?jīng)典的“我以后可能會換一種方式實(shí)現(xiàn)它”面前,都變得似乎需要被解耦。這種理由,聽上去合理,其實(shí)是狗屁。換一種方式實(shí)現(xiàn)它,并不意味著要用一個(gè)接口來抽象它,接口是用來抽象并解耦依賴關(guān)系的,應(yīng)該被用在:同時(shí)存在多個(gè)實(shí)現(xiàn)、實(shí)現(xiàn)未知或需要模塊化的情況下(還有一種情況,是方便多人開發(fā)時(shí)工作內(nèi)容的解耦,但我還沒有想明白,引入接口來達(dá)到這個(gè)目的是否合適:因管理需要導(dǎo)致的復(fù)雜度上升。所以先不討論這種情況)。
具體解釋一下,“同時(shí)存在多個(gè)實(shí)現(xiàn)”的意思。以IComparable接口為例,很多數(shù)據(jù)類(比如DTO)大都實(shí)現(xiàn)了這個(gè)接口,因?yàn)樯蠈拥墓δ埽ū热缗判颍┮蕾囶惖膶ο笥邢嗷ケ容^的能力,同時(shí)每個(gè)類的實(shí)現(xiàn)方式又都不一樣,即所謂的同時(shí)存在多個(gè)實(shí)現(xiàn)。
所以,對于需要“換一種方式實(shí)現(xiàn)它”的情況,大可以把原來的代碼刪除然后重新寫一個(gè)。
有句話叫“拿著錘子,看什么都像釘子”。了解一項(xiàng)技術(shù),不僅僅要了解他能做什么,更要了解這個(gè)技術(shù)適用在什么地方。所以千萬別今天聽了解耦的概念覺得很前衛(wèi),第二天就去把所有的類都提取出個(gè)接口。多數(shù)情況當(dāng)然不會這么夸張,但濫用其實(shí)就在一念之間。
接口的壞味道
我承認(rèn),上面解釋也許正確,但沒什么用。懂的人懂,不懂的還是不懂;所以我還是舉些接口有問題的壞味道吧。
最常見的接口壞味兒包括:(注意,總可以找到反例,所以一開始就說了,沒有準(zhǔn)則,總要具體問題具體分析,但是如果使用接口的原因是如下幾種之下,我覺得應(yīng)該再仔細(xì)考慮一下)
-
為了提取出某一個(gè)類所提供的Public方法。接口應(yīng)該用來抽象依賴,而不是抽象實(shí)現(xiàn)。后面再解釋。你想知道或控制一個(gè)類有哪些Method的方法有很多,但是引入一個(gè)接口,不僅達(dá)不到你的目的,還引入了復(fù)雜度——每當(dāng)你要加一個(gè)方法,都要修改兩個(gè)地方,一個(gè)是接口,一個(gè)是實(shí)現(xiàn)。
-
接口抽象出來了,但是和實(shí)現(xiàn)放了一起,或者根本沒用到這個(gè)接口。比如,如果你寫出了:
Interface f = new Implementation();
這樣的代碼,而且這個(gè)接口只被這樣用過,那或許需要考慮一下使用這個(gè)接口的用法了。我并不是指你需要一個(gè)依賴注入的框架。但是這至少看上去不太對勁,像是為了使用接口而提取出了這個(gè)接口。
-
接口中包含了互不相關(guān)的方法。如果某個(gè)方法出現(xiàn)在這個(gè)接口里會讓人覺得驚訝,那這個(gè)接口就是有問題的。不能因?yàn)橛袃蓚€(gè)以上的類都有這個(gè)方法,所以就提取出來了。要看這兩個(gè)方法有沒有關(guān)系,還要看上層是不是一定會同時(shí)依賴這兩個(gè)方法。使用者使用接口中的方法時(shí),應(yīng)該全部都用得到。如果沒全用到,可能需要考慮一下這個(gè)接口劃分的是否合理?的粒度是不是太粗了?還是把接口當(dāng)成了Common Service Host來用了?
同一張類圖的不同解釋——真假DIP
扯得有點(diǎn)兒遠(yuǎn)了?;貋砝^續(xù)正題,考慮如何把User和Implementation解耦合。所有人都知道,解耦的方法是:
- 定義接口I
- Implementation實(shí)現(xiàn)接口I
- User使用接口I,則不是Implementation。
這個(gè)描述已經(jīng)很細(xì)了,而且畫出來的類圖也是唯一的。但是很可惜,這個(gè)描述是不明確的,有歧義的。
代碼2和代碼5都符合這個(gè)描述,但是其實(shí)是不同的設(shè)計(jì)。用圖來描述會更清楚一些。
圖2
圖3
或許有人一看到學(xué)術(shù)派的設(shè)計(jì)圖就興奮起來,一眼就看出有一個(gè)設(shè)計(jì)是有問題的。但是當(dāng)你看到代碼2時(shí),你有一眼看出問題嗎?到你自己的項(xiàng)目代碼中,你能一眼看出問題嗎?問題總是出現(xiàn)在“混亂”中,簡化成圖2、圖3這樣,只要知道DIP的人,恐怕都能看出問題。但到項(xiàng)目中,那就是另一回事兒了。就像多數(shù)人都很鄙視國家組織的“軟考”,考得再好,也不表示有相當(dāng)?shù)脑O(shè)計(jì)水平。這種簡化了的問題和考題一樣,也許能明白,但是能在該用的時(shí)候記得用,并不是個(gè)容易的事兒。
我來解釋一下,其中根本的區(qū)別在于誰依賴誰。至于誰持有接口,只是表象。從邏輯上,調(diào)用方很明顯地依賴著實(shí)現(xiàn)方,因?yàn)閷?shí)現(xiàn)方才是功能的實(shí)現(xiàn)者,沒有實(shí)現(xiàn)方,調(diào)用方就工作不了。但是在圖3的設(shè)計(jì)中,其設(shè)計(jì)意圖是,實(shí)現(xiàn)方要實(shí)現(xiàn)的功能,由調(diào)用方來決定,而不是實(shí)現(xiàn)方實(shí)現(xiàn)了什么,調(diào)用方就用什么。也就是說,要讓實(shí)現(xiàn)方依賴調(diào)用方。這,就是DIP(依賴倒置原則)的含義。其具體表現(xiàn)就是,調(diào)用方定義并持有接口。
從概念上來講,DIP的定義如下2:
- 高層次的模塊不應(yīng)該依賴于低層次的模塊,他們都應(yīng)該依賴于抽象。
- 抽象(Abstractions)不應(yīng)該依賴于實(shí)現(xiàn)(details),實(shí)現(xiàn)應(yīng)該依賴于抽象。
目前在網(wǎng)上找到的對DIP的解釋,多數(shù)都停留在第一項(xiàng),即模塊依賴抽象上,都沒有解釋清楚“倒置”這個(gè)詞的含義。希望本文中的圖2和圖3解釋清楚了“倒置”的含義。從概念上來講,“抽象不應(yīng)該依賴于實(shí)現(xiàn)”,就是要求“倒置”。因?yàn)槿绻駡D2那種思路,從實(shí)現(xiàn)中抽象出接口,那么這個(gè)接口就是依賴于實(shí)現(xiàn)的。重復(fù)一下之前說過的:接口,應(yīng)該是對依賴的抽象,而不是對實(shí)現(xiàn)(底層功能)的抽象,這就是所謂的倒置。(這里的依賴的含義是,調(diào)用者所需要的功能,而不是實(shí)現(xiàn)者實(shí)現(xiàn)了的功能。)
另外,還是這個(gè)類圖,還有一種常見的組織形式。像下面這樣。
圖4
從箭頭的方向上來看,這個(gè)更倒置。但是模塊的細(xì)分,箭頭方向的顛倒,并不意味著這個(gè)設(shè)計(jì)真的是倒置的。這要取決于抽象層中的接口,是與圖2中的接口定位一致呢?還是與圖3中的接口定位一致?單純地把接口放在抽象層里,就和單純地定義一個(gè)接口,卻沒有地方用到它一樣沒有意義。
所以說,清楚地表達(dá)一個(gè)設(shè)計(jì),并能讓人確切地明白你的設(shè)計(jì)。其實(shí)是一件非常不容易的事情??赡馨裊ML的所有功能都用上,才能做到這一點(diǎn)。僅僅畫個(gè)框框、線線、寫倆字兒,是很容易讓人誤會的。開會的時(shí)候有人解釋著還好,如果寫出的文檔如果是這樣,對新手而言還不如沒有,因?yàn)榛旧弦欢〞徽`解。
了解DIP有什么用?DIP能用在什么地方?
我猜不少人看到這里會很想問,知道“倒置”到底是什么意思有個(gè)鳥用?有好的創(chuàng)意去開發(fā)項(xiàng)目才是正經(jīng)事兒,把項(xiàng)目按時(shí)保質(zhì)地做出來才是正經(jīng)事兒,老子按時(shí)下班才是正經(jīng)事兒。
首先,我非常同意!然后,回答這個(gè)問題,這個(gè)每個(gè)人的個(gè)性使然。就像天天研究吃什么健康有個(gè)鳥用?中國的食品安全都保證不了,還健康?!但是就是有人就好這口,不是么?而且,我在這里只是解釋DIP,也并沒有說做的項(xiàng)目里,都要符合DIP啊。項(xiàng)目管理和架構(gòu)是很靈活的,不是幾個(gè)P就可以規(guī)范的起來的。有時(shí)候,直接找個(gè)開源的產(chǎn)品一搭,多快好省,一個(gè)P也用不著。如果非要給出個(gè)理由,我想恬不知恥地說句,追求卓越。(好吧,根本原因是,我喜歡得瑟,但是又不喜歡被明白人罵成豬頭,所以我選擇先搞明白了再去得瑟。)
但是我還是要說說了解這個(gè)原則的好處,不然寫這文章不是打自己臉么?了解依賴倒置的意義,并不限于設(shè)計(jì),還在于思想上的轉(zhuǎn)變。理解這個(gè)原則之后,你會發(fā)現(xiàn)自己明明已經(jīng)把這個(gè)原則用上了,比如做需求分析的時(shí)候,肯定是問用戶想要什么,而不是我們能做到什么。
這個(gè)原則在協(xié)作上也有用處。請回想一下,在工作中,是否遇到過上層開發(fā)人員等下層開發(fā)接口的情況呢?如果遇到過,當(dāng)時(shí)有沒有想過,這個(gè)依賴關(guān)系是不是反了呢?其實(shí),應(yīng)該是下層模塊的開發(fā)者依賴上層開發(fā)者呀。上層開發(fā)者定義好他依賴的接口,下層開發(fā)者來實(shí)現(xiàn),同時(shí),因?yàn)榻涌谝呀?jīng)定義好了,上層也不用等下層開發(fā)者,完全可以用些Mock框架進(jìn)行測試嘛。但是,如果讓下層開發(fā)者定義接口,顯然上層開發(fā)者就必須等,Mock類也寫不了。
關(guān)于這個(gè)原則,我還見到過更廣義,更天下大同的解釋。在客戶關(guān)系上,我們常見的依賴是開發(fā)者依賴客戶,客戶說什么我們就得做什么,一點(diǎn)主動(dòng)權(quán)都沒有。于是有人就把依賴倒置的原則拿來,說,應(yīng)該讓客戶依賴開發(fā)者!大有,“我們說什么,客戶就聽什么!”的派頭。到底哪個(gè)依賴是倒置的我就不在這兒爭了,因?yàn)槲矣X得這完全不是依賴的方向性問題。而是店大欺客還是客大欺店的問題。如果你在IBM、在SAP、在四大,你可以讓客戶聽你的。如果你在一個(gè)小屁公司,或者客戶是政府部門,你倒置個(gè)試試?
下回預(yù)告
自此之后,一切安好。
直到有一天,又有一個(gè)用戶,他的燈上的開關(guān)也壞了,然后他試著把另外一家廠商的開關(guān)裝了上去,卻發(fā)現(xiàn)打不開燈。用戶抱怨道,他的這個(gè)開關(guān)可是按國際標(biāo)準(zhǔn)實(shí)現(xiàn)的,我們的燈具應(yīng)該支持這種標(biāo)準(zhǔn)開關(guān)。
如果有可能,我們一定會讓這個(gè)燈支持這個(gè)國際標(biāo)準(zhǔn)??墒菬粢呀?jīng)賣出去了,出廠的千千萬萬個(gè)燈都召回的代價(jià)也很大。
這個(gè)燈的設(shè)計(jì),又要做出怎樣的變化呢?
原文鏈接:http://www.cnblogs.com/nankezhishi/archive/2012/05/26/dip.html
【編輯推薦】
- 項(xiàng)目模塊開發(fā)——切dvd庫
- 公開我的開源項(xiàng)目newland.js
- Silverlight 3D開源項(xiàng)目
- 常見的產(chǎn)品需求獲取來源
- JavaScript項(xiàng)目優(yōu)化總結(jié)
網(wǎng)站名稱:小例子背后的大道理:從DIP中“倒置”的含義說接口
轉(zhuǎn)載來于:http://fisionsoft.com.cn/article/dhhjsoj.html


咨詢
建站咨詢
