第四回:依賴的哲學(xué)(上)
本文將介紹以下內(nèi)容:
1 引言
這幾個(gè)關(guān)鍵的問題,所以我決定不單純的通過DIP而DIP,而是從依賴這個(gè)最原始的概念講起,來了解在面向?qū)ο筌浖O(shè)計(jì)體系中,關(guān)于“關(guān)系的處 理”,也就是“依賴的哲學(xué)”。對,依賴就是關(guān)系,處理依賴也就意味著處理關(guān)系。因?yàn)?,我們?nèi)祟愂亲钌朴诟汴P(guān)系的動(dòng)物,所以原本可以簡單的理論,在人類的意 識(shí)哲學(xué)中變得復(fù)雜而多變,以至于我們本應(yīng)簡單的道理變得如此復(fù)雜,這就是依賴。那么,從依賴講起來了解依賴倒置原則,我覺得首先應(yīng)該回到以下的問題:
帶著對這些問題的思考和思索,Anytao帶領(lǐng)大家就依賴這個(gè)話題開始一次循序漸進(jìn)的面向?qū)ο笾?,以解答這些從一開始就有足夠吸引力的問題,從原理到實(shí)例,從關(guān)系到異同,我期待這篇文章能帶來一些認(rèn)知的變革。 2 什么是依賴,什么是抽象 2.1 關(guān)于依賴和耦合:由小國寡民到和諧社會(huì) 在老子的“小國寡民”論中,提出了一種理想的社會(huì)狀態(tài):民至老死,不相往來。這是他老人家的一種社會(huì)理想,老死不相往來的人群呈現(xiàn)了一片和諧景象。
因?yàn)椴话l(fā)生瓜葛,也就無所謂關(guān)聯(lián),進(jìn)而無法倒置沖突。這是先祖哲學(xué)中的至純哲理,但理想的大同總是和現(xiàn)實(shí)的生態(tài)有著或多或少的差距,人類社會(huì)無法避免聯(lián)系
的發(fā)生,所以小國寡民的理想成為一種美麗的夢想,不可實(shí)現(xiàn)。同樣的道理,映射到軟件“社會(huì)”中,也就是軟件系統(tǒng)結(jié)構(gòu)中,也預(yù)示著不同的層次、模塊、類型之
間也必然存在著或多或少的聯(lián)系,這種聯(lián)系不可避免但可管理。正如人類社會(huì)雖然無法實(shí)現(xiàn)小國寡民,但是理想的狀態(tài)下我們推崇和諧社會(huì),把人群的聯(lián)系由復(fù)雜變
為簡單,由曲折變?yōu)榻y(tǒng)一,同樣可以使得這種關(guān)聯(lián)很和諧。所以,軟件系統(tǒng)的使命也應(yīng)該朝著和諧社會(huì)的目標(biāo)前進(jìn),對于不同的關(guān)系處理,使用一套行之有效的哲
學(xué),把復(fù)雜問題簡單化,把僵化問題柔性化,這種哲學(xué)或者說方法,在我看來就是:依賴的哲學(xué),也就是本文所要闡釋的中心思想。
依賴,就是關(guān)系,代表了軟件實(shí)體之間的聯(lián)系。軟件的實(shí)體可能是模塊,可能是層次,也可能是具體的類型,不同的實(shí)體直接發(fā)生依賴,也就意味著發(fā)生了耦 合。所以,依賴和耦合在我看來是對一個(gè)問題的兩種表達(dá),依賴闡釋了耦合本質(zhì),而耦合量化了依賴程度。因此,我們對于關(guān)系的描述方式,就可以從兩個(gè)方面的觀 點(diǎn)來分析:
隨著軟件世界的革命,業(yè)務(wù)邏輯的復(fù)雜,以上的簡單化處理已經(jīng)不足以實(shí)現(xiàn)更復(fù)雜的軟件產(chǎn)品,在系統(tǒng)內(nèi)部的復(fù)雜度成為一個(gè)超越人腦可識(shí)別的程度時(shí),例如:
因此,人類開始發(fā)揮重組和簡單化處理的優(yōu)勢,我們不得不在軟件設(shè)計(jì)上做出平衡。平衡的結(jié)果就是通過對復(fù)雜的系統(tǒng)模塊化,把復(fù)雜問題簡單處理,從而達(dá) 到能夠被人腦識(shí)別的目的?;谶@種指導(dǎo)原則,隨著復(fù)雜度的增加模塊的劃分更加朝著精細(xì)化發(fā)展,尤其是面向?qū)ο蟪绦蛟O(shè)計(jì)理論的出現(xiàn),使得對復(fù)雜的處理實(shí)現(xiàn)了 更科學(xué)的理論基礎(chǔ)。然而,復(fù)雜的問題可以通過劃分實(shí)現(xiàn)簡單的功能模塊或者技術(shù)單元,但由此應(yīng)運(yùn)而生的子單元會(huì)越來越多,而且越來越多的子單元必須發(fā)生數(shù)據(jù) 的通信才能完成統(tǒng)一的業(yè)務(wù)處理,所以產(chǎn)生的數(shù)據(jù)通信管理也越來越多。對于子單元的管理,也就是我們本文關(guān)注的核心概念-依賴,成為新的軟件設(shè)計(jì)問題,那么 總結(jié)前人的經(jīng)驗(yàn),提煉今人的智慧,我們對耦合的產(chǎn)生做以如下歸納:
了解了耦合發(fā)生的一般方式,我們就可以進(jìn)入了核心思想的討論,那就是在認(rèn)識(shí)依賴和了解依賴的基礎(chǔ)上,我們最終追求的目標(biāo)。
討論了半天,終于是時(shí)候?qū)σ蕾嚭婉詈线M(jìn)行一點(diǎn)兒總結(jié)了,也是該進(jìn)行一點(diǎn)目標(biāo)訴求了。在軟件設(shè)計(jì)領(lǐng)域,有那么幾個(gè)至高原則值得我們深刻心中,它們是:
對了,就是這些平凡的字眼,匯集了面向?qū)ο笏枷氲暮诵膬?nèi)容,也是本文力求闡釋的禪意心經(jīng)。關(guān)于面向抽象編程和封裝變化,我們會(huì)在后面詳細(xì)闡釋,在此我們需要將注意力關(guān)注于“低耦合,高內(nèi)聚”這一目標(biāo)。
2.2 關(guān)于抽象和具體 什么是抽象呢?我們首先不必澄清什么是抽象,而從什么算抽象說起,穩(wěn)定的、高層的則代表了抽象。就像一個(gè)公司,最好保證了高層的穩(wěn)定,才能保證全局
的發(fā)展。在進(jìn)行系統(tǒng)設(shè)計(jì)時(shí),穩(wěn)定的抽象接口和高層邏輯,也代表了整個(gè)系統(tǒng)的穩(wěn)定與柔性。兵熊熊一窩,將良良一窩,系統(tǒng)的邏輯也正如著代表打仗,良好的設(shè)計(jì)
都是自上而下的。而對具體的編程實(shí)踐而言,接口和抽象類則代表了語言層次的抽象。
因此,為了分離這種緊耦合,最好的辦法就是隔離,引入中間層來分離變化,同時(shí)確保中間層本身的穩(wěn)定性,因此抽象的中間層是最佳的選擇。
例如: public interface IUserService { } public class UserService : IUserService { } 下面依賴于具體: public class UserManager { private UserService service = null; } 下面依賴于抽象: public class UserManager { private IUserService service = null; } 二者的區(qū)別僅在于引入了接口IUserService,從而使得UserManager對于UserService的依賴由強(qiáng)減弱。這種方式也在我們的Ezsocio項(xiàng)目中進(jìn)行service層的設(shè)計(jì)方式。然而對于依賴的方式并非僅此一種,設(shè)計(jì)模式中的智慧正是通過各章編程技巧進(jìn)行依賴關(guān)系的設(shè)計(jì),值得我們關(guān)注和學(xué)習(xí),本文也在下文進(jìn)行相關(guān)設(shè)計(jì)模式的討論。
由需求而設(shè)計(jì)的過程,就是一個(gè)分散集中化的過程,把需求相關(guān)的業(yè)務(wù)通過開發(fā)流程的需求分析過程進(jìn)行整理,逐步形成需求規(guī)格說明、概要設(shè)計(jì)和詳細(xì)設(shè)計(jì) 等基本流程。分散集中化,是一個(gè)梳理需求到形成設(shè)計(jì)的過程,因此對于把握系統(tǒng)中的抽象和具體而言,是一個(gè)重要的分析過程和手段。現(xiàn)代軟件工程已經(jīng)對此形成 了科學(xué)的標(biāo)準(zhǔn)化流程處理邏輯,例如可以借助UML更加清晰的設(shè)計(jì)流程、分析設(shè)計(jì)要素,進(jìn)行標(biāo)準(zhǔn)化溝通和交流。
將具體問題抽象化,是本節(jié)關(guān)注的要點(diǎn),而處理的方法是什么呢?答案就在設(shè)計(jì)模式,設(shè)計(jì)模式是前輩智慧的總結(jié)和實(shí)踐,所以熟悉和學(xué)習(xí)設(shè)計(jì)模式,是學(xué)習(xí) 和實(shí)踐設(shè)計(jì)問題的必經(jīng)之路。然而,沒有哪個(gè)問題是由設(shè)計(jì)模式全權(quán)解決,也沒有那個(gè)模式能夠適應(yīng)所有的問題,因此我們要努力的是盡量積累更多的模式來應(yīng)對多 變的需求。作為軟件設(shè)計(jì)話題中最重量級(jí)的話題,我也會(huì)在以后的歲月中對設(shè)計(jì)模式問題進(jìn)行一些探討。
總的來說,抽象和變化就像一對孿生兄弟,將具體的變化點(diǎn)隔離出來以抽象的方式進(jìn)行封裝,在變化的地方尋找抽象是面對抽象最理想的方式。所以,如何去
尋找變化是設(shè)計(jì)要解決的首頁問題,例如工廠模式的目標(biāo)是封裝對象創(chuàng)建的變化,橋接模式封裝的是對象間的依賴關(guān)系變化等等。23個(gè)經(jīng)典的設(shè)計(jì)模式,從某種角
度來看,正是對不同變化點(diǎn)的封裝角度提出的不同解決方案。 2.3 設(shè)計(jì)的哲學(xué) 之所以花如此篇幅來講述一個(gè)看似簡單的問題,其實(shí)最終理想是回歸到軟件設(shè)計(jì)目標(biāo)這個(gè)命題上。如果悉心鉆研就可發(fā)現(xiàn),設(shè)計(jì)的最后就是對關(guān)系的處理,正
如同生活的意義在于對社會(huì)的適應(yīng)一樣。因此,回歸到設(shè)計(jì)的目標(biāo)上我們就可知,完美的設(shè)計(jì)過程就是對關(guān)系的處理過程,也就是對依賴的梳理過程,并最終形成一
種合理的耦合結(jié)果。
在此,IDataProvider作為隔離業(yè)務(wù)層和數(shù)據(jù)層的抽象,IService作為隔離業(yè)務(wù)層和表現(xiàn)層的抽象,保證了各個(gè)層次的相對穩(wěn)定和封
裝。而體現(xiàn)在此的設(shè)計(jì)邏輯,就正是我們對于抽象和耦合基本目標(biāo)概念的體現(xiàn),例如作為重用的單元,抽象隔離保證了對外發(fā)布接口的單一和穩(wěn)定,所以達(dá)到了最高
限度的重用;通過引入中間的穩(wěn)定的接口,達(dá)到了不同層次的有效隔離,層與層之間體現(xiàn)為輕度耦合,業(yè)務(wù)層只持有IDataProvider就可以獲取數(shù)據(jù)層
的所有服務(wù),而表現(xiàn)層也同樣如此;最后,這種方式顯然也直接實(shí)踐了面向接口編程,面向抽象編程的經(jīng)典理念。 3 認(rèn)識(shí)依賴倒置原則(DIP) 3.1 什么是依賴倒置? Bob大叔在《Agile Principles, Patterns, and Practices》一書中對依賴倒置原則進(jìn)行了精辟的總結(jié)為:
我規(guī)規(guī)矩矩一字不差的把上述真言放在心里,卻發(fā)現(xiàn)大師的牛論實(shí)在有點(diǎn)故作玄虛,就像欣賞Bob在論述DIP時(shí)的插畫一樣費(fèi)解不討好:
其實(shí)著名的好萊塢原則更形象的闡述了這一思想:你不要調(diào)我,我來調(diào)你。不管是通俗的還是高尚的,卻都不約而同的揭示了依賴倒置原則的最核心思想就是: 依賴于抽象,對接口編程,對抽象編程! 相較而言,從實(shí)際的生活中來看依賴倒置,就像下面這個(gè)示例揭示的一樣。 3.2 從實(shí)例開始 綜合對依賴倒置的認(rèn)識(shí),結(jié)合到具體的程序?qū)崿F(xiàn)而言,依賴倒置預(yù)示著程序中的依賴關(guān)系不應(yīng)是具體的類型,而是歸咎于抽象類和接口。下面我們通過一個(gè)簡
單的實(shí)例來分析符合依賴倒置和違反依賴倒置,對于系統(tǒng)設(shè)計(jì)的影響和區(qū)別。我們的需求是為某個(gè)遙控器生產(chǎn)商,實(shí)現(xiàn)一個(gè)萬能遙控器,該遙控器可以對當(dāng)前市場上
的很多電子設(shè)備進(jìn)行“打開”和“關(guān)閉”的操作,例如你可以使用Anytao牌遙控器打開海爾電視、創(chuàng)維電視等等,當(dāng)然更理想的狀態(tài)是可以打開電冰箱、電燈
還有門窗等等,總之凡是可以互聯(lián)的設(shè)備都是未來萬能遙控器的新需求。
隨后,廠商多了一個(gè)重量級(jí)客戶長虹,所以小王不得不對初試設(shè)計(jì)進(jìn)行了改造,勉強(qiáng)適應(yīng)了新的需求,如下: 雖然小王應(yīng)付了這次需求變動(dòng),但是原本的設(shè)計(jì)顯然已經(jīng)捉襟見肘。正當(dāng)小王絞盡腦汁進(jìn)行改造的同時(shí),新的需求接踵而來:新飛冰箱、飛利浦照明、盼盼防盜門,一個(gè)接一個(gè)。小王的最終設(shè)計(jì)變成了這般摸樣:
哎,真是太累了。每一次的需求變更都伴隨著小王對遙控器Remote的再次摧殘,Remote內(nèi)部不斷增加新的引用和操作處理,顯然一個(gè)
if/else式的判斷布滿了整個(gè)Open和Close的操作中,這種設(shè)計(jì)顯然無法滿足OCP對擴(kuò)展開放、對修改封閉的要求。顯然,如果想讓賣出去的遙控
器也適應(yīng)新的需求,在小王當(dāng)前的設(shè)計(jì)實(shí)現(xiàn)方案中是根本無法實(shí)現(xiàn)的,遙控器廠商總不能召回已經(jīng)售出所有的遙控器,再拆開進(jìn)行重新改造吧。
在當(dāng)前的設(shè)計(jì)中,老王的思路是讓遙控器廠切斷和各個(gè)廠家的直接聯(lián)系,而是尋找所有電視廠商的領(lǐng)導(dǎo)(例如,電視機(jī)協(xié)會(huì)),請電視機(jī)協(xié)會(huì)制定所有電視機(jī) 廠商必須遵守的打開和關(guān)閉等操作的契約,遙控器廠和電視機(jī)協(xié)會(huì)建立直接的聯(lián)系而不是各個(gè)具體的電視廠商,于是便有了上述設(shè)計(jì)思路。而新的需求來臨時(shí),因?yàn)? 各個(gè)廠商必須遵守TurnOn和TurnOff的契約,所以輕松的萬能遙控器可以應(yīng)付所有的電視機(jī)品牌,實(shí)現(xiàn)的具體操作已經(jīng)由遙控器轉(zhuǎn)移到具體的廠商手上 (順便說說這也是所有權(quán)的倒置體現(xiàn)),輕松的小王終于大呼一口氣。并且再接再厲修改了更完善的版本: 現(xiàn)在,遙控器基本實(shí)現(xiàn)了萬能的要求,任何新的需求或者修改都可以輕松勝任。小王終于解決了原本設(shè)計(jì)的所有問題,帶著感激盛情邀請老王吃飯致謝。席間
就坐,小王請教老王二次設(shè)計(jì)的秘訣,老王神秘一笑沾酒在桌子上寫了幾個(gè)大字:依賴倒置。經(jīng)歷此次設(shè)計(jì)重構(gòu)洗禮的小王,也在實(shí)戰(zhàn)中體味了設(shè)計(jì)的精妙,看著依
賴倒置幾個(gè)字小王也會(huì)心的笑了。 3.3 為什么依賴倒置? 依賴倒置原則揭示了面向?qū)ο笏枷胫幸粋€(gè)最基本而最核心的話題,那就是:面向抽象編程。任何對依賴倒置原則的違反都不同程度的偏離了面向?qū)ο笤O(shè)計(jì)思想的軌道,所以如果你想自己的程序是否足夠的OO,透徹的了解依賴倒置是必不可少的。
3.4 為什么是倒置? 魯迅先生有云:其實(shí)地上本沒有路,走的人多了也便成了路。對依賴倒置原則中的“倒置”二字而言,其實(shí)也類似于一條被很多人走過的路,因?yàn)榱?xí)慣性的稱呼走過的為“路”,所以只好把違反習(xí)慣的東西稱為“倒置的路”。這倒置的含義,正基于此。 高層模塊通過自上而下的實(shí)現(xiàn)來完成系統(tǒng)功能的調(diào)用,將這種方式表達(dá)為代碼就是: // Release : code01, 2008/11/02
// Author : Anytao, http://www.
public static void Main() { try
{ //Do something here.
} catch
{ Log(true, "XMLLog"); } } public static void Log(bool isRead, string logType) { if (isRead)
ReadLog(logType); else
WriteLog(logType); } 然而,當(dāng)軟件設(shè)計(jì)的模式發(fā)展到面向?qū)ο箅A段時(shí),我們發(fā)現(xiàn)原來習(xí)慣的世界了已經(jīng)變了?;诟邔右蕾囉诘讓拥谋渍?,也越來越被可擴(kuò)展性的系統(tǒng)需求折磨的 面目全非,例如如果日志記錄的載體發(fā)生變化,當(dāng)前設(shè)計(jì)中需要同時(shí)自上而下的修改實(shí)現(xiàn)的邏輯,同時(shí)避免出現(xiàn)越來越多的if/else結(jié)構(gòu)。所以當(dāng)新的依賴關(guān) 系從傳統(tǒng)的方式被完全扭轉(zhuǎn)時(shí),“倒置”二字就此誕生了。我們修改Log實(shí)現(xiàn)的設(shè)計(jì)思路,將可能變化的邏輯封裝為抽象接口,使得高層依賴發(fā)生轉(zhuǎn)換:
程序?qū)崿F(xiàn)的邏輯早已被面向?qū)ο蟮脑O(shè)計(jì)思想所取代,我們新的實(shí)現(xiàn)變成了: // Release : code02, 2008/11/02
// Author : Anytao, http://www.
public class Client { public static void Main() { ILog myLogger = new XMLLog();
try
{ } catch
{ myLogger.Write(); } } } public interface ILog { void Read();
void Write();
} public class XMLLog : ILog { public void Read() { } public void Write() { } } 所以,了解了歷史才能正視現(xiàn)實(shí),對于軟件設(shè)計(jì)同樣如此,只有認(rèn)清楚依賴倒置產(chǎn)生的歷史背景,我們才能更加熟練的駕馭倒置含義本身帶來的誤解,而將中心思想牢牢的把握在依賴倒置最核心的設(shè)計(jì)思想上,那還是萬變不離其宗的:依賴于抽象,這簡單的5個(gè)字上。 3.5 如何依賴倒置? 如何依賴倒置的關(guān)鍵,還是體現(xiàn)在如何對抽象和具體的封裝和分離,實(shí)踐的基本思路就是封裝變化。這正如我們在單一職責(zé)原則中反復(fù)強(qiáng)調(diào),對一個(gè)類只有一
個(gè)引起它變化的原因。我們實(shí)踐依賴倒置,仍然可以從關(guān)注變化開始,詳細(xì)的分析和預(yù)測系統(tǒng)中的變化點(diǎn),然后針對每個(gè)可能的變化抽象出相對穩(wěn)定的約束,這是我
們實(shí)踐依賴倒置原則最基本的方法步驟。
實(shí)際上,在實(shí)際的設(shè)計(jì)過程中要完全遵守這幾點(diǎn)要求是有難度的,所以如何既能很好的遵守設(shè)計(jì)原則,又能很好的適應(yīng)代碼情況,是值得權(quán)衡的問題,需要我們不斷的積累和實(shí)踐。另外,還有幾個(gè)經(jīng)驗(yàn)只談:
3.6 也有弊端 然而,一味的遵守原則,就等于沒有原則。重要的是,我們需要把握其平衡,在進(jìn)行開發(fā)中適當(dāng)?shù)陌盐掌涑潭?。Bob在《敏捷》中也提到這個(gè)問題,他總結(jié)了依賴倒置的兩個(gè)弊端,同樣需要我們必要的關(guān)注:
所以,學(xué)習(xí)模式或者原則必須把握靈活處理,不能一味強(qiáng)行。
|
|