正交設(shè)計(jì),是普遍的設(shè)計(jì)原則,與粒度無(wú)關(guān),與編程范式無(wú)關(guān),更與具體的實(shí)現(xiàn)語(yǔ)言無(wú)關(guān)。(雖然確實(shí)在不同的編程范式下,或使用不同的編程語(yǔ)言時(shí),具體的解決方法或難易程度不同,這也正是為何我們總是在尋找更適合的編程范式,更高效的編程語(yǔ)言的原因)。
而具體到面向?qū)ο?/strong>范式,我們都知道著名的SOLID原則。但是:這五個(gè)原則是怎么來(lái)的?它們的目的何在?它們的關(guān)系如何?
為了搞清楚這些疑問(wèn),我們?cè)俅位氐阶畛醯膯?wèn)題:
- 軟件模塊該如何劃分?(怎么分)
- 模塊間API該如何定義?(怎么合)
怎樣進(jìn)行分解?
模塊的劃分,是一個(gè)問(wèn)題分解的過(guò)程。
在文章《VBD (Volatility Based Decomposition)》里,引用了一篇70年代的論文《On the Criteria to be Used in Decomposing Systems into Modules》。
在這篇論文里,作者通過(guò)一個(gè)小例子,清晰的指出了軟件模塊劃分應(yīng)該以基于信息隱藏為目的,以職責(zé)劃分為手段,從而封裝變化,讓軟件更加容易修改(即Kent Beck
的理想:局部化影響)。
這篇文章也展示了:基于流程(過(guò)程)的分解,是一種極其脆弱的模塊劃分方式。因而我們應(yīng)該離基于過(guò)程的分解越遠(yuǎn)越好。
這也是為何Matt Cochran將這種分解方法稱做:基于易變性的分解(Volatility Based Decomposition)。
總而言之,變化及應(yīng)對(duì)變化,是軟件設(shè)計(jì)最大的挑戰(zhàn),目的和意義。
面向?qū)ο缶烤挂鉀Q什么問(wèn)題?
最近幾年,我聽(tīng)到太多針對(duì)OO
的批評(píng),其中最為奇葩的說(shuō)法是:OO
只適合GUI
編程。因?yàn)?code>GUI組件概念上更接近于對(duì)象,至于其它領(lǐng)域則不太合適。
對(duì)提出這樣高論的人,我相當(dāng)確信,他們不僅對(duì)于面向?qū)ο?/strong>一無(wú)所知,更是對(duì)于復(fù)雜軟件所面臨的真正挑戰(zhàn),以及軟件設(shè)計(jì)究竟要解決什么問(wèn)題一無(wú)所知。
前兩天偶然從東海陳光劍的文章《函數(shù)式編程與面向?qū)ο缶幊蘙5]:編程的本質(zhì)》讀到軟件模塊化的目的和價(jià)值(雖然他用的是結(jié)構(gòu)化,但在我看來(lái)也是在談模塊化),非常精彩,深合我意:
(軟件設(shè)計(jì)是一個(gè))層次化分解與重新復(fù)合的過(guò)程
這個(gè)思維過(guò)程, 并非是受計(jì)算機(jī)的限制而產(chǎn)生,它反映的是人類思維的局限性。我們的大腦一次只能處理很少的概念。生物學(xué)中被廣為引用的 一篇論文指出我們我們的大腦中只能保存
7 ± 2
個(gè)信息塊。我們對(duì)人類短期記憶的認(rèn)識(shí)可能會(huì)有變化,但是可以肯定的是它是有限的。底線就是我們不能處理一大堆亂糟糟的對(duì)象或像蘭州拉面似的代碼。我們需要結(jié)構(gòu)化并非是因?yàn)榻Y(jié)構(gòu)化的程序看上去有多么美好,而是我們的大腦無(wú)法有效的處理非結(jié)構(gòu)化的東西。我們經(jīng)常說(shuō)一些代碼片段是優(yōu)雅的或美觀的,實(shí)際上那只意味 著它們更容易被人類有限的思維所處理。優(yōu)雅的代碼創(chuàng)造出尺度合理的代碼塊,它正好與我們的『心智消化系統(tǒng)』能夠吸收的數(shù)量相符。
那么,對(duì)于程序的復(fù)合而言,正確的代碼塊是怎樣的?它們的表面積必須要比它們的體積增長(zhǎng)的更為緩慢。我喜歡這個(gè)比喻,因?yàn)閹缀螌?duì)象的表面積是以尺寸的平方的速度增長(zhǎng)的,而體積是以尺寸的立方的速度增長(zhǎng)的,因此表面積的增長(zhǎng)速度小于體積。
代碼塊的表面積是是我們復(fù)合代碼塊時(shí)所需要的信息。代碼塊的體積 是我們?yōu)榱藢?shí)現(xiàn)它們所需要的信息。一旦代碼塊的實(shí)現(xiàn)過(guò)程結(jié)束,我們就可以忘掉它的實(shí)現(xiàn)細(xì)節(jié),只關(guān)心它與其他代碼塊的相互影響。在面向?qū)ο缶幊讨?,類或接口的聲明就是表面。在函?shù)式編程中,函數(shù)的聲明就是表面。我把事情簡(jiǎn)化了一些,但是要點(diǎn)就是這些。
怎樣才能做到表面積增長(zhǎng)速度小于體積增長(zhǎng)速度?當(dāng)然是分解,信息隱藏,抽象。而這些也正是面向?qū)ο?/strong>所追求和擅長(zhǎng)的。
面向?qū)ο?/strong>主要目的是提供一種語(yǔ)言級(jí)的模塊化支持。雖然在一些數(shù)學(xué)家看來(lái):由于面向?qū)ο?/strong>沒(méi)有很好的數(shù)學(xué)理論基礎(chǔ),因而必然是一個(gè)錯(cuò)誤的方法論。可對(duì)于如何編寫(xiě)一個(gè)易于應(yīng)對(duì)變化的軟件,并不是一個(gè)純數(shù)學(xué)理論問(wèn)題(或許確實(shí)有數(shù)學(xué)家也可以抽象出一套數(shù)學(xué)理論),而更多的是一個(gè)實(shí)踐問(wèn)題。作為長(zhǎng)期處于實(shí)踐一線的我們,不應(yīng)把幾個(gè)數(shù)學(xué)家的看法當(dāng)作金科玉律(對(duì)于那些沒(méi)有實(shí)踐經(jīng)驗(yàn),卻對(duì)如何實(shí)踐指手畫(huà)腳的純理論派,每次看到他們的不負(fù)責(zé)任的言論,考慮到他們的影響力和對(duì)新手的誤導(dǎo),就禁不住想對(duì)他們豎中指)(參見(jiàn)《學(xué)習(xí)的邏輯3: 三人行必有我徒》)。
軟件工業(yè)最近20年來(lái),能夠構(gòu)建如此大規(guī)模的需求頻繁變化的軟件系統(tǒng),很大程度上得益于面向?qū)ο?/strong>對(duì)于模塊化的良好支持。
而現(xiàn)在風(fēng)頭正勁的微服務(wù)化,無(wú)非是把模塊化的思想,從進(jìn)程內(nèi)模塊(類),變?yōu)檫M(jìn)程間而已。
OO和FP
面向?qū)ο?/strong>與函數(shù)式編程的最大區(qū)別在于數(shù)據(jù)是否是強(qiáng)制不變性。這個(gè)前提,導(dǎo)致了一系列其它的差異。
因?yàn)榭勺冃裕?strong>面向?qū)ο?/strong>可以將算法和數(shù)據(jù)放在一起,當(dāng)數(shù)據(jù)是一種實(shí)現(xiàn)細(xì)節(jié)時(shí),可對(duì)其進(jìn)行信息隱藏和封裝。但在Pure FP
里,數(shù)據(jù)和算法是必須分離的。這種分離,在很多場(chǎng)景下,對(duì)于信息隱藏相當(dāng)不利(在這里我們先不談性能)。因而,當(dāng)系統(tǒng)規(guī)模足夠復(fù)雜時(shí),FP
對(duì)于構(gòu)造易于維護(hù)軟件的能力比面向?qū)ο?/strong>要弱。
因而,FP
為了實(shí)用,要么部分放棄對(duì)不變性的堅(jiān)持(如LISP
所做的那樣),從而允許模擬面向?qū)ο蠓妒剑▍⒁?jiàn)SICP);要么通過(guò)Existential Quantification
,來(lái)模擬OO
的運(yùn)行時(shí)多態(tài),以達(dá)到信息隱藏,隔離變化的目的;要么使用輕量級(jí)進(jìn)程(輕量很關(guān)鍵):讓每個(gè)輕量級(jí)進(jìn)程承擔(dān)一個(gè)很小的職責(zé),從進(jìn)程外部看,每個(gè)輕量級(jí)進(jìn)程都可以有可修改的數(shù)據(jù),以及基于消息的行為驅(qū)動(dòng)(如erlang
或akka
的Actor
模型),而這正是對(duì)于smalltalk
的對(duì)象模擬,從而緩解了不變性帶來(lái)的尷尬。進(jìn)而也說(shuō)明了基于高內(nèi)聚,低耦合原則進(jìn)行的模塊化是超越范式的。
因而,一些FPer
對(duì)于OO
的盲目批評(píng),和認(rèn)為面向?qū)ο?/strong>只適合GUI
領(lǐng)域一樣,都并不真正明白一個(gè)復(fù)雜軟件的關(guān)鍵挑戰(zhàn),以及解決方案何在。
當(dāng)然,具體到編程語(yǔ)言,即便都是OO
語(yǔ)言,差別也巨大。但這是另外一個(gè)宏大的話題,這里暫且不談。只重點(diǎn)說(shuō)一句:不要把某種OO
語(yǔ)言,當(dāng)作OO
本身。
關(guān)于FP
和OO
的話題,值得專門(mén)寫(xiě)一篇文章全面論述,而本文的目的在于介紹SOLID
,因而不再贅述。
正交原則與SOLID的關(guān)系
一個(gè)好的面向?qū)ο笤O(shè)計(jì),自然是符合高內(nèi)聚,低耦合原則的對(duì)象劃分和協(xié)作方式。
單一職責(zé)和開(kāi)放封閉,更多的在強(qiáng)調(diào)類劃分時(shí)的高內(nèi)聚;而里氏替換,依賴倒置,接口隔離則更多的強(qiáng)調(diào)類與類之間協(xié)作接口(即API)定義的低耦合。
高內(nèi)聚(怎么分)
單一職責(zé),通過(guò)對(duì)變化原因的識(shí)別,將一個(gè)承擔(dān)多重職責(zé)的類,不斷分割為更小的,只具備單一變化原因的類。而單一變化原因指的是:一個(gè)變化,會(huì)引起整個(gè)類都發(fā)生變化。只有關(guān)聯(lián)極其緊密的情況,才會(huì)導(dǎo)致這樣的局面。因而,單一職責(zé)和高內(nèi)聚某種程度是同義詞。
但單一職責(zé)原則本身,并沒(méi)有明確指示我們?cè)撊绾闻卸ㄒ粋€(gè)類屬于單一職責(zé)的,以及如何達(dá)到單一職責(zé)的狀態(tài)。而策略消除重復(fù),分離不同變化方向,正是讓類達(dá)到單一職責(zé)的策略與途徑。


低耦合 (怎么合)
而開(kāi)放封閉原則,正是通過(guò)將不同變化方向進(jìn)行分離,從而達(dá)到對(duì)于已經(jīng)出現(xiàn)的變化方向,對(duì)于修改是封閉的,對(duì)于擴(kuò)展是開(kāi)放的。

里氏替換原則強(qiáng)調(diào)的是,一個(gè)子類不應(yīng)該破壞其父類與客戶之間的契約。唯有如此,才能保證:客戶與其父類所暴露的接口(即API)所產(chǎn)生的依賴關(guān)系是穩(wěn)定的。子類只應(yīng)該成為隱藏在API背后的某種具體實(shí)現(xiàn)方式。

依賴倒置原則則強(qiáng)調(diào):為了讓依賴關(guān)系是穩(wěn)定的,不應(yīng)該由實(shí)現(xiàn)側(cè)根據(jù)自己的技術(shù)實(shí)現(xiàn)方式定義接口,然后強(qiáng)迫上層(即客戶)依賴這種不穩(wěn)定的API定義,而是應(yīng)該站在上層(即客戶)的角度去定義API(正所謂依賴倒置)。
但是,雖然接口由上層定義,但最終接口的實(shí)現(xiàn)卻依然由下層完成,因此依賴倒置描述為:上層不依賴下層,下層也不依賴上層,雙方共同依賴于抽象。

最后,接口隔離原則強(qiáng)調(diào)的是:不應(yīng)該強(qiáng)迫客戶依賴它不需要的東西。顯然,這是縮小依賴范圍策略在面向?qū)ο蠓妒较碌漠a(chǎn)物。

結(jié)論
正交設(shè)計(jì)是一種與范式,語(yǔ)言無(wú)關(guān)的設(shè)計(jì)原則。為了解決在模塊化的過(guò)程中,如何讓軟件在長(zhǎng)期范圍內(nèi)更容易應(yīng)對(duì)變化。
而面向?qū)ο?/strong>是一種對(duì)模塊化支持良好的范式。通過(guò)高內(nèi)聚,低耦合原則,或正交策略的運(yùn)用,面向?qū)ο?/strong>范式下SOLID
原則會(huì)自然浮現(xiàn)。
我們耳熟能詳?shù)能浖O(shè)計(jì)相關(guān)原則,模式與實(shí)踐的關(guān)系如下:

關(guān)于正交設(shè)計(jì)的更多細(xì)節(jié),請(qǐng)參閱《變化驅(qū)動(dòng):正交設(shè)計(jì)》。