成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

開發 架構
軟件項目中根據需求進行設計的工作,有很多原理性的方法。本文將給大家揭示一些小例子背后的大道理。

開燈的例子

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

用代碼實現這樣一個有開關功能的燈,也是一件很容易的事情。

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

代碼1

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

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

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

  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

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

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

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

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

暗流涌動

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

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

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

代碼3

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

     一個可行的設計是,讓開關支持收音機的開啟和停止。像下面這樣。

  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的實現方式違反了OCP(開放—封閉原則),如果這樣發展下去,公司的產品越豐富,這坨代碼就越難以維護。我的末日也就越近。

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

Guru眼中的依賴

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

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

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

  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,只是各種名稱上的變化。結構上一點兒沒變。

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

DIP(依賴倒置原則)

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

    假設有如下所示的類圖。假設我們要把這種關系解耦合。

clip_image002[7]

圖1

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

為什么要解耦合?

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

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

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

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

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

接口的壞味道

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

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

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

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

    Interface f = new Implementation();

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

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

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

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

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

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

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

clip_image004[4]

圖2

clip_image006[4]

圖3

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

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

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

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

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

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

clip_image008[4]

圖4

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

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

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

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

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

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

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

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

    自此之后,一切安好。

    直到有一天,又有一個用戶,他的燈上的開關也壞了,然后他試著把另外一家廠商的開關裝了上去,卻發現打不開燈。用戶抱怨道,他的這個開關可是按國際標準實現的,我們的燈具應該支持這種標準開關。

    如果有可能,我們一定會讓這個燈支持這個國際標準。可是燈已經賣出去了,出廠的千千萬萬個燈都召回的代價也很大。

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

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

【編輯推薦】

責任編輯:彭凡 來源: 博客園
相關推薦

2012-08-20 10:48:09

2016-11-15 13:52:19

2018-05-10 16:21:19

產品

2017-11-29 12:56:02

人工智能大數據成語

2016-05-09 10:38:36

樣本量選擇

2020-02-14 14:05:10

刪庫跑路發生

2022-07-08 14:47:47

比特幣虛擬貨幣貸幣

2021-04-07 14:45:56

軟件測試編程

2024-05-10 07:19:46

IOC依賴倒置控制反轉

2023-08-03 17:08:05

Linux退出碼

2016-11-22 19:54:56

點擊率預估推薦算法廣告

2015-10-20 15:00:51

七牛云

2024-07-02 11:05:03

依賴倒置系統

2012-06-11 10:27:43

英特爾主板雷電接口

2011-06-12 09:08:46

字母索引

2020-06-22 17:42:37

華為

2012-01-06 10:12:25

2010-10-25 10:13:16

ibmdwWebSphere

2023-05-04 07:44:13

編程界小語言Java
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲二区在线 | 日韩成人高清在线 | 欧美第一页 | 一级a爱片久久毛片 | 久久久久国产精品一区二区 | 日韩欧美精品在线 | 一区二区免费视频 | 久久天堂 | 日日噜噜噜夜夜爽爽狠狠视频97 | 亚洲国产高清免费 | 欧美一级欧美三级在线观看 | 91精品欧美久久久久久久 | 国产电影一区二区三区爱妃记 | 久久久久久久久久久久91 | 久久久综合久久 | 日韩av三区 | 一区二区亚洲 | 国内自拍偷拍 | 国产精品亚洲第一区在线暖暖韩国 | 国产精品久久久久久久久久久免费看 | 国产精品欧美一区二区 | 免费在线观看黄色av | 一区欧美| 在线观看视频福利 | 亚洲一区二区在线视频 | 97人人超碰 | 国产精品成人一区二区三区夜夜夜 | 日韩国产在线 | 亚洲一区二区在线电影 | 中文字幕综合 | 婷婷综合五月天 | 亚洲欧洲成人av每日更新 | 国产日韩精品一区 | 二区三区av | 日韩在线一区二区 | 九九热国产视频 | 日韩欧美三级电影 | 久久国产精品久久 | 日本高清中文字幕 | aaaaaa大片免费看最大的 | 91视频在线 |