自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

小例子背后的大道理:從DIP中“倒置”的含義說接口

開發(fā) 架構(gòu)
軟件項目中根據(jù)需求進(jìn)行設(shè)計的工作,有很多原理性的方法。本文將給大家揭示一些小例子背后的大道理。

開燈的例子

選開燈做例子,是因為這個例子既常見又簡單,而且潛在的需求多樣。對于最簡單的燈,從功能上講,按下燈上的開關(guān),燈就開了。

用代碼實現(xiàn)這樣一個有開關(guān)功能的燈,也是一件很容易的事情。

  1. public class Light 
  2.     public void TurnOn() { Console.WriteLine("Light Turn On"); } 
  3.     public void TurnOff() { Console.WriteLine("Light Turn Off"); } 

代碼1

一個具有開關(guān)功能的燈就完成了。這個燈,功能完備、也滿足當(dāng)下的需求。一切美好。

     直到有一天,有個客戶說,燈上的開關(guān)壞了,能不能換一個?我才意識到這個燈的設(shè)計有問題——它的開關(guān)是換不了的。一面給用戶解釋,一面考慮著把燈和開關(guān)分開。

     咱也是學(xué)過設(shè)計模式的人,知道要面向接口編程,絕不應(yīng)該簡單地把Light類拆解成Light和Switcher兩個類。因為Switcher不應(yīng)該依賴于具體實現(xiàn),于是寫出了下面的代碼。

  1. namespace Me.Lighting 
  2.     public interface ILightable 
  3.     { 
  4.         void ShowLight(); 
  5.         void HideLight(); 
  6.     } 
  7.     public class Light : ILightable 
  8.     { 
  9.         public void ShowLight() { Console.WriteLine("Light Turn On"); } 
  10.         public void HideLight() { Console.WriteLine("Light Turn Off"); } 
  11.     } 
  12. namespace Me.Switch 
  13.     using Me.Lighting; 
  14.   
  15.     public class Switcher 
  16.     { 
  17.         public ILightable Light { getset; } 
  18.         public void TurnOn() { Light.ShowLight(); } 
  19.         public void TurnOff() { Light.HideLight(); } 
  20.     } 

代碼 2

     這個設(shè)計,不僅分離了燈和開關(guān),甚至可以讓這個開關(guān)靈活地控制要開關(guān)哪個燈。只要在開關(guān)前設(shè)置一下就可以,多方便。我自信滿滿地遷入了代碼。

     事實也證明這樣的設(shè)計是成功的,產(chǎn)品的靈活設(shè)計得到了用戶的認(rèn)可,銷量直線上升。

      親,請看下代碼,在不使用什么別的設(shè)計模式的前提下,您覺得代碼2有什么問題?無論是什么角度的都可以(當(dāng)然,可能您的角度不是本文討論的重點),最好先回復(fù)下留個底,別事后諸葛。

     如果您一眼看到了問題,請直接閱讀DIP那一節(jié)。

暗流涌動

      公司壯大之后 ,開始考慮向收音機(jī)行業(yè)進(jìn)軍。而且公司希望,這種靈活的設(shè)計可以沿用下去,收音機(jī)和燈的開關(guān)應(yīng)該可以通用,對用戶而言,都是撥那么一下。

      我聽到這個信息也是相當(dāng)興奮,但是當(dāng)我開始著手寫代碼時,發(fā)現(xiàn)一些壞味道,開關(guān)依賴于ILightable 接口,那么我的收音機(jī)不得不寫成這個樣子才能與現(xiàn)有的開關(guān)兼容。

  1. public class Radio : ILightable 
  2. {  
  3.     public void ShowLight() { Console.WriteLine("Play radio"); } 
  4.     public void HideLight() { Console.WriteLine("Stop radio"); }  

代碼3

     雖然可以工作,但是這是嚴(yán)重的壞味道。因為如果有一天,燈的接口變化,我卻要連收音機(jī)的代碼一起改。這種情況絕不應(yīng)該出現(xiàn)。且不用把LSP(Liskov替換原則)搬出來說教,很顯然Radio其實并沒有完成ILightable所定義的功能——發(fā)光。無論從哪個角度講都是錯的。

     一個可行的設(shè)計是,讓開關(guān)支持收音機(jī)的開啟和停止。像下面這樣。

  1. namespace Me.Radio 
  2.     public interface IRadio 
  3.     { 
  4.         void Play(); 
  5.         void Stop(); 
  6.     } 
  7.     public class Radio : IRadio 
  8.     { 
  9.         public void Play() { Console.WriteLine("Play radio"); } 
  10.         public void Stop() { Console.WriteLine("Stop radio"); } 
  11.     } 
  12. namespace Me.Switch 
  13.     using Me.Lighting; 
  14.     using Me.Radio; 
  15.   
  16.     public class Switcher 
  17.     { 
  18.         public ILightable Light { getset; } 
  19.         public IRadio Radio { getset; } 
  20.         public void TurnOn() 
  21.         { 
  22.             if (Light != null) Light.ShowLight(); 
  23.             else if (Radio != null) Radio.Play(); 
  24.         } 
  25.         public void TurnOff() { Light.HideLight(); } 
  26.     } 

代碼4

     我看來看去都覺得這個代碼太惡心了,因為Switcher的實現(xiàn)方式違反了OCP(開放—封閉原則),如果這樣發(fā)展下去,公司的產(chǎn)品越豐富,這坨代碼就越難以維護(hù)。我的末日也就越近。

     于是我的考慮Switcher的設(shè)計是不是有問題,我已經(jīng)用上面向接口編程了,為什么還是有問題呢?

Guru眼中的依賴

      我把代碼發(fā)給了我的導(dǎo)師,一個設(shè)計Guru,他看完之后哭笑著說,你的基本功很扎實,理論知識也很全面,可惜卻缺乏一定的經(jīng)驗。面向接口編程沒有錯,但是更重要的是模型的建立。

     簡單而言,你的開關(guān)的依賴關(guān)系錯了。問你一個問題你就明白了,開關(guān)為什么要依賴ILightable呢?但是好在你有一定的設(shè)計基礎(chǔ),知道要提取出一個接口,所以要改成正確的設(shè)計也非常容易。你只需要把ILightable這個接口的名字改成ISwitchable,再把接口方法名字改下,并把它與Switcher放一起就行了。

    聽罷,我恍然大悟。原來接口的名字和位置,也會給使用者帶來如此大的困擾。在先進(jìn)的開發(fā)工具的幫助下,瞬間就完成了這個簡單的重命名和移動操作?,F(xiàn)在的代碼像這個樣子了。

  1. namespace Me.Lighting 
  2.     using Me.Switch; 
  3.     public class Light : ISwitchable 
  4.     { 
  5.         public void TurnOn() { Console.WriteLine("Light Turn On"); } 
  6.         public void TurnOff() { Console.WriteLine("Light Turn Off"); } 
  7.     } 
  8. namespace Me.Radio 
  9.     using Me.Switch; 
  10.     public class Radio : ISwitchable 
  11.     { 
  12.         public void TurnOn() { Console.WriteLine("Play radio"); } 
  13.         public void TurnOff() { Console.WriteLine("Stop radio"); } 
  14.     } 
  15. namespace Me.Switch 
  16.     public interface ISwitchable 
  17.     {  
  18.         void TurnOn(); 
  19.         void TurnOff(); 
  20.     } 
  21.     public class Switcher 
  22.     { 
  23.         public ISwitchable Switchee { getset; }  
  24.         public void TurnOn() { Switchee.TurnOn(); }  
  25.         public void TurnOff() { Switchee.TurnOff(); } 
  26.     } 

代碼5

    注意:這個代碼與之前有問題的代碼2,只是各種名稱上的變化。結(jié)構(gòu)上一點兒沒變。

    以后有新的產(chǎn)品,也只需要實現(xiàn)ISwitchable接口,就可以支持這個開關(guān)了。之前的失敗設(shè)計,看似與這個設(shè)計相差無幾,但是其中蘊含的設(shè)計思想天差地遠(yuǎn),也正是在這種地方,才更能體現(xiàn)出設(shè)計師間的差距。這一種設(shè)計所體現(xiàn)的,即是DIP(依賴倒置原則),的表現(xiàn)之一,接口應(yīng)當(dāng)被其使用者所擁有,而非其實現(xiàn)者。1

DIP(依賴倒置原則)

    具體問題解決了,還需要把整個問題抽象一下,從本質(zhì)上了解一下DIP的含義。(我會盡量清楚,可能會有些啰嗦,但這比在回復(fù)里爭論要舒坦得多。)

    假設(shè)有如下所示的類圖。假設(shè)我們要把這種關(guān)系解耦合。

clip_image002[7]

圖1

注:圖1中的User表示使用者(調(diào)用者),而不是用戶的意思。

為什么要解耦合?

     我說“假設(shè)要解耦合”,是因為在嘗試解耦這種依賴關(guān)系之前,應(yīng)該先確定有沒有解耦的必要。這種關(guān)系在代碼中比比皆是,如果把所有的依賴都解耦,不僅工作量大、帶不來任何好處,而且引入了不必要的復(fù)雜度,最終演變成了過度設(shè)計,增加了編碼成本和維護(hù)成本。(我已經(jīng)被人罵怕了,怕不說清楚這一點,總要有人跳出來說我濫用模式,說這種關(guān)系要不要解耦要看情況,云云。都是好意,我也心領(lǐng)了,謝謝。但被人假設(shè)狗屁不通,總不太舒服。)

     明確某個依賴關(guān)系是否需要被分解,是一件很復(fù)雜的事情,個人覺得并沒有什么準(zhǔn)則能讓你輕松地做出這個判斷。因為幾乎所有的依賴,在一句經(jīng)典的“我以后可能會換一種方式實現(xiàn)它”面前,都變得似乎需要被解耦。這種理由,聽上去合理,其實是狗屁。換一種方式實現(xiàn)它,并不意味著要用一個接口來抽象它,接口是用來抽象并解耦依賴關(guān)系的,應(yīng)該被用在:同時存在多個實現(xiàn)、實現(xiàn)未知或需要模塊化的情況下(還有一種情況,是方便多人開發(fā)時工作內(nèi)容的解耦,但我還沒有想明白,引入接口來達(dá)到這個目的是否合適:因管理需要導(dǎo)致的復(fù)雜度上升。所以先不討論這種情況)。

     具體解釋一下,“同時存在多個實現(xiàn)”的意思。以IComparable接口為例,很多數(shù)據(jù)類(比如DTO)大都實現(xiàn)了這個接口,因為上層的功能(比如排序)依賴類的對象有相互比較的能力,同時每個類的實現(xiàn)方式又都不一樣,即所謂的同時存在多個實現(xiàn)。

     所以,對于需要“換一種方式實現(xiàn)它”的情況,大可以把原來的代碼刪除然后重新寫一個。

     有句話叫“拿著錘子,看什么都像釘子”。了解一項技術(shù),不僅僅要了解他能做什么,更要了解這個技術(shù)適用在什么地方。所以千萬別今天聽了解耦的概念覺得很前衛(wèi),第二天就去把所有的類都提取出個接口。多數(shù)情況當(dāng)然不會這么夸張,但濫用其實就在一念之間。

接口的壞味道

     我承認(rèn),上面解釋也許正確,但沒什么用。懂的人懂,不懂的還是不懂;所以我還是舉些接口有問題的壞味道吧。

     最常見的接口壞味兒包括:(注意,總可以找到反例,所以一開始就說了,沒有準(zhǔn)則,總要具體問題具體分析,但是如果使用接口的原因是如下幾種之下,我覺得應(yīng)該再仔細(xì)考慮一下)

  1. 為了提取出某一個類所提供的Public方法。接口應(yīng)該用來抽象依賴,而不是抽象實現(xiàn)。后面再解釋。你想知道或控制一個類有哪些Method的方法有很多,但是引入一個接口,不僅達(dá)不到你的目的,還引入了復(fù)雜度——每當(dāng)你要加一個方法,都要修改兩個地方,一個是接口,一個是實現(xiàn)。

  2. 接口抽象出來了,但是和實現(xiàn)放了一起,或者根本沒用到這個接口。比如,如果你寫出了:

    Interface f = new Implementation();

    這樣的代碼,而且這個接口只被這樣用過,那或許需要考慮一下使用這個接口的用法了。我并不是指你需要一個依賴注入的框架。但是這至少看上去不太對勁,像是為了使用接口而提取出了這個接口。

  3. 接口中包含了互不相關(guān)的方法。如果某個方法出現(xiàn)在這個接口里會讓人覺得驚訝,那這個接口就是有問題的。不能因為有兩個以上的類都有這個方法,所以就提取出來了。要看這兩個方法有沒有關(guān)系,還要看上層是不是一定會同時依賴這兩個方法。使用者使用接口中的方法時,應(yīng)該全部都用得到。如果沒全用到,可能需要考慮一下這個接口劃分的是否合理?的粒度是不是太粗了?還是把接口當(dāng)成了Common Service Host來用了?

    同一張類圖的不同解釋——真假DIP

    扯得有點兒遠(yuǎn)了。回來繼續(xù)正題,考慮如何把User和Implementation解耦合。所有人都知道,解耦的方法是:

  1. 定義接口I
  2. Implementation實現(xiàn)接口I
  3. User使用接口I,則不是Implementation。

    這個描述已經(jīng)很細(xì)了,而且畫出來的類圖也是唯一的。但是很可惜,這個描述是不明確的,有歧義的。

    代碼2和代碼5都符合這個描述,但是其實是不同的設(shè)計。用圖來描述會更清楚一些。

clip_image004[4]

圖2

clip_image006[4]

圖3

      或許有人一看到學(xué)術(shù)派的設(shè)計圖就興奮起來,一眼就看出有一個設(shè)計是有問題的。但是當(dāng)你看到代碼2時,你有一眼看出問題嗎?到你自己的項目代碼中,你能一眼看出問題嗎?問題總是出現(xiàn)在“混亂”中,簡化成圖2、圖3這樣,只要知道DIP的人,恐怕都能看出問題。但到項目中,那就是另一回事兒了。就像多數(shù)人都很鄙視國家組織的“軟考”,考得再好,也不表示有相當(dāng)?shù)脑O(shè)計水平。這種簡化了的問題和考題一樣,也許能明白,但是能在該用的時候記得用,并不是個容易的事兒。

     我來解釋一下,其中根本的區(qū)別在于誰依賴誰。至于誰持有接口,只是表象。從邏輯上,調(diào)用方很明顯地依賴著實現(xiàn)方,因為實現(xiàn)方才是功能的實現(xiàn)者,沒有實現(xiàn)方,調(diào)用方就工作不了。但是在圖3的設(shè)計中,其設(shè)計意圖是,實現(xiàn)方要實現(xiàn)的功能,由調(diào)用方來決定,而不是實現(xiàn)方實現(xiàn)了什么,調(diào)用方就用什么。也就是說,要讓實現(xiàn)方依賴調(diào)用方。這,就是DIP(依賴倒置原則)的含義。其具體表現(xiàn)就是,調(diào)用方定義并持有接口。

     從概念上來講,DIP的定義如下2:

  1. 高層次的模塊不應(yīng)該依賴于低層次的模塊,他們都應(yīng)該依賴于抽象。
  2. 抽象(Abstractions)不應(yīng)該依賴于實現(xiàn)(details),實現(xiàn)應(yīng)該依賴于抽象。

     目前在網(wǎng)上找到的對DIP的解釋,多數(shù)都停留在第一項,即模塊依賴抽象上,都沒有解釋清楚“倒置”這個詞的含義。希望本文中的圖2和圖3解釋清楚了“倒置”的含義。從概念上來講,“抽象不應(yīng)該依賴于實現(xiàn)”,就是要求“倒置”。因為如果像圖2那種思路,從實現(xiàn)中抽象出接口,那么這個接口就是依賴于實現(xiàn)的。重復(fù)一下之前說過的:接口,應(yīng)該是對依賴的抽象,而不是對實現(xiàn)(底層功能)的抽象,這就是所謂的倒置。(這里的依賴的含義是,調(diào)用者所需要的功能,而不是實現(xiàn)者實現(xiàn)了的功能。)

另外,還是這個類圖,還有一種常見的組織形式。像下面這樣。

clip_image008[4]

圖4

     從箭頭的方向上來看,這個更倒置。但是模塊的細(xì)分,箭頭方向的顛倒,并不意味著這個設(shè)計真的是倒置的。這要取決于抽象層中的接口,是與圖2中的接口定位一致呢?還是與圖3中的接口定位一致?單純地把接口放在抽象層里,就和單純地定義一個接口,卻沒有地方用到它一樣沒有意義。

     所以說,清楚地表達(dá)一個設(shè)計,并能讓人確切地明白你的設(shè)計。其實是一件非常不容易的事情??赡馨裊ML的所有功能都用上,才能做到這一點。僅僅畫個框框、線線、寫倆字兒,是很容易讓人誤會的。開會的時候有人解釋著還好,如果寫出的文檔如果是這樣,對新手而言還不如沒有,因為基本上一定會被誤解。

了解DIP有什么用?DIP能用在什么地方?

     我猜不少人看到這里會很想問,知道“倒置”到底是什么意思有個鳥用?有好的創(chuàng)意去開發(fā)項目才是正經(jīng)事兒,把項目按時保質(zhì)地做出來才是正經(jīng)事兒,老子按時下班才是正經(jīng)事兒。

     首先,我非常同意!然后,回答這個問題,這個每個人的個性使然。就像天天研究吃什么健康有個鳥用?中國的食品安全都保證不了,還健康?!但是就是有人就好這口,不是么?而且,我在這里只是解釋DIP,也并沒有說做的項目里,都要符合DIP啊。項目管理和架構(gòu)是很靈活的,不是幾個P就可以規(guī)范的起來的。有時候,直接找個開源的產(chǎn)品一搭,多快好省,一個P也用不著。如果非要給出個理由,我想恬不知恥地說句,追求卓越。(好吧,根本原因是,我喜歡得瑟,但是又不喜歡被明白人罵成豬頭,所以我選擇先搞明白了再去得瑟。)

     但是我還是要說說了解這個原則的好處,不然寫這文章不是打自己臉么?了解依賴倒置的意義,并不限于設(shè)計,還在于思想上的轉(zhuǎn)變。理解這個原則之后,你會發(fā)現(xiàn)自己明明已經(jīng)把這個原則用上了,比如做需求分析的時候,肯定是問用戶想要什么,而不是我們能做到什么。

     這個原則在協(xié)作上也有用處。請回想一下,在工作中,是否遇到過上層開發(fā)人員等下層開發(fā)接口的情況呢?如果遇到過,當(dāng)時有沒有想過,這個依賴關(guān)系是不是反了呢?其實,應(yīng)該是下層模塊的開發(fā)者依賴上層開發(fā)者呀。上層開發(fā)者定義好他依賴的接口,下層開發(fā)者來實現(xiàn),同時,因為接口已經(jīng)定義好了,上層也不用等下層開發(fā)者,完全可以用些Mock框架進(jìn)行測試嘛。但是,如果讓下層開發(fā)者定義接口,顯然上層開發(fā)者就必須等,Mock類也寫不了。

     關(guān)于這個原則,我還見到過更廣義,更天下大同的解釋。在客戶關(guān)系上,我們常見的依賴是開發(fā)者依賴客戶,客戶說什么我們就得做什么,一點主動權(quán)都沒有。于是有人就把依賴倒置的原則拿來,說,應(yīng)該讓客戶依賴開發(fā)者!大有,“我們說什么,客戶就聽什么!”的派頭。到底哪個依賴是倒置的我就不在這兒爭了,因為我覺得這完全不是依賴的方向性問題。而是店大欺客還是客大欺店的問題。如果你在IBM、在SAP、在四大,你可以讓客戶聽你的。如果你在一個小屁公司,或者客戶是政府部門,你倒置個試試?

    自此之后,一切安好。

    直到有一天,又有一個用戶,他的燈上的開關(guān)也壞了,然后他試著把另外一家廠商的開關(guān)裝了上去,卻發(fā)現(xiàn)打不開燈。用戶抱怨道,他的這個開關(guān)可是按國際標(biāo)準(zhǔn)實現(xiàn)的,我們的燈具應(yīng)該支持這種標(biāo)準(zhǔn)開關(guān)。

    如果有可能,我們一定會讓這個燈支持這個國際標(biāo)準(zhǔn)。可是燈已經(jīng)賣出去了,出廠的千千萬萬個燈都召回的代價也很大。

    這個燈的設(shè)計,又要做出怎樣的變化呢?

原文鏈接:http://www.cnblogs.com/nankezhishi/archive/2012/05/26/dip.html

【編輯推薦】

責(zé)任編輯:彭凡 來源: 博客園
相關(guān)推薦

2012-08-20 10:48:09

2016-11-15 13:52:19

2018-05-10 16:21:19

產(chǎn)品

2017-11-29 12:56:02

人工智能大數(shù)據(jù)成語

2016-05-09 10:38:36

樣本量選擇

2020-02-14 14:05:10

刪庫跑路發(fā)生

2022-07-08 14:47:47

比特幣虛擬貨幣貸幣

2021-04-07 14:45:56

軟件測試編程

2024-05-10 07:19:46

IOC依賴倒置控制反轉(zhuǎn)

2023-08-03 17:08:05

Linux退出碼

2024-07-02 11:05:03

依賴倒置系統(tǒng)

2012-01-06 10:12:25

2012-06-11 10:27:43

英特爾主板雷電接口

2021-07-15 10:11:56

IT流程設(shè)計流程流程文化

2020-06-22 17:42:37

華為

2015-10-20 15:00:51

七牛云

2023-05-04 07:44:13

編程界小語言Java

2010-10-25 10:13:16

ibmdwWebSphere

2011-11-07 09:26:51

域樹
點贊
收藏

51CTO技術(shù)棧公眾號