正交設(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)生,它反映的是人類(lèi)思維的局限性僻爽。我們的大腦一次只能處理很少的概念虫碉。生物學(xué)中被廣為引用的 一篇論文指出我們我們的大腦中只能保存
7 ± 2
個(gè)信息塊。我們對(duì)人類(lèi)短期記憶的認(rèn)識(shí)可能會(huì)有變化胸梆,但是可以肯定的是它是有限的敦捧。底線就是我們不能處理一大堆亂糟糟的對(duì)象或像蘭州拉面似的代碼。我們需要結(jié)構(gòu)化并非是因?yàn)榻Y(jié)構(gòu)化的程序看上去有多么美好碰镜,而是我們的大腦無(wú)法有效的處理非結(jié)構(gòu)化的東西兢卵。我們經(jīng)常說(shuō)一些代碼片段是優(yōu)雅的或美觀的,實(shí)際上那只意味 著它們更容易被人類(lèi)有限的思維所處理绪颖。優(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ū)ο缶幊讨衅】妫?lèi)或接口的聲明就是表面。在函數(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)模塊(類(lè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)類(lèi)劃分時(shí)的高內(nèi)聚浪漠;而里氏替換,依賴倒置霎褐,接口隔離則更多的強(qiáng)調(diào)類(lèi)與類(lèi)之間協(xié)作接口(即API)定義的低耦合址愿。
高內(nèi)聚(怎么分)
單一職責(zé),通過(guò)對(duì)變化原因的識(shí)別冻璃,將一個(gè)承擔(dān)多重職責(zé)的類(lèi)响谓,不斷分割為更小的,只具備單一變化原因的類(lèi)省艳。而單一變化原因指的是:一個(gè)變化娘纷,會(huì)引起整個(gè)類(lèi)都發(fā)生變化。只有關(guān)聯(lián)極其緊密的情況跋炕,才會(huì)導(dǎo)致這樣的局面赖晶。因而,單一職責(zé)和高內(nèi)聚某種程度是同義詞辐烂。
但單一職責(zé)原則本身遏插,并沒(méi)有明確指示我們?cè)撊绾闻卸ㄒ粋€(gè)類(lèi)屬于單一職責(zé)的,以及如何達(dá)到單一職責(zé)的狀態(tài)纠修。而策略消除重復(fù)涩堤,分離不同變化方向,正是讓類(lèi)達(dá)到單一職責(zé)的策略與途徑分瘾。
低耦合 (怎么合)
而開(kāi)放封閉原則胎围,正是通過(guò)將不同變化方向進(jìn)行分離吁系,從而達(dá)到對(duì)于已經(jīng)出現(xiàn)的變化方向,對(duì)于修改是封閉的白魂,對(duì)于擴(kuò)展是開(kāi)放的汽纤。
里氏替換原則強(qiáng)調(diào)的是,一個(gè)子類(lèi)不應(yīng)該破壞其父類(lèi)與客戶之間的契約福荸。唯有如此蕴坪,才能保證:客戶與其父類(lèi)所暴露的接口(即API)所產(chǎn)生的依賴關(guān)系是穩(wěn)定的。子類(lèi)只應(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ì)》。