從“高內(nèi)聚,低耦合”說起
記得在上學的時候,?師就說過“?內(nèi)聚,低耦合”,但當初對這句話的理解?較淺顯。?作之后,為了說服別?采???設(shè)計的?案,常常說“……這樣就做到了?內(nèi)聚,低耦合……”。
隨著?作經(jīng)驗越來越豐富,學到的內(nèi)容越來越多,在解釋設(shè)計?案的時候,可能會這樣說:
“……此處使?了策略模式,從?保證了模塊相對的穩(wěn)定性,和較強的擴展性……”;
“……這個聚合維護了A、 B和C之間的固定規(guī)則……”。
?之前經(jīng)常說的“?內(nèi)聚,低耦合”卻不會經(jīng)常掛在嘴邊了。那什么是“?內(nèi)聚,低耦合”呢?
內(nèi)聚性和耦合性,都是軟件度量。內(nèi)聚性是指功能相關(guān)的程序組合成?個模塊的程度,或是各機能凝聚的狀態(tài)或程度。耦合性是指,?個程序中模塊及模塊之間信息或參數(shù)依賴的程度。它們都是早期結(jié)構(gòu)化分析的重要概念之?,?且是相對的概念。?般內(nèi)聚性?的程序,通常是低耦合的。雖然我們不再使?嚴格的“結(jié)構(gòu)化分析”步驟,但是它依然適?于現(xiàn)在?直存在的模塊關(guān)系中。在?向?qū)ο蟮姆治龊驮O(shè)計中,?個類可以看成最?的模塊,那么內(nèi)聚性和耦合性也可以表達為對象之間的關(guān)系?!案邇?nèi)聚,低耦合”代表著這程序更健壯、更易擴展。
它似乎并未過時,但是在討論問題的時候,我們?yōu)槭裁床辉俳?jīng)常使??或者說你在什么時候使?呢?或者我們把問題再擴??點——你依據(jù)什么“設(shè)計?法”去指導架構(gòu)設(shè)計?作呢?是SOLID?是設(shè)計模式?還是DDD呢?
在回答這個問題之前,我們看看還有哪些“設(shè)計?法”出現(xiàn)在分析和設(shè)計?作中,以及使?時遇到的問題。
令人迷茫的設(shè)計方法
故事一 SOLID不夠明確?
對OO設(shè)計熟悉的讀者肯定知道著名的SOLID原則。當時我對OO的認識還只有“封裝、繼承和多態(tài)”,讀到這些理論的時候,收獲頗多。直到有同事問我:“單?職責原則規(guī)定每個類都有單?的功能,那么到底?個類有多少功能算是單?呢?如果?個類有多個成員?法,是不是?定要拆分成多個類呢? ”
我想了?下,如果?個類中只有CRUD?法,是不是要把類拆成Creator、 Retriver、 Updater和Deleter才能滿?這個原則呢??認為對SOLID了如指掌的我?時?語凝噎。
故事二 設(shè)計模式違反“原則”?
接觸到設(shè)計模式后,“策略模式”中的每?個算法被封裝到?個類,算是滿?了“單?職責”,并且“針對了接?編程,?不是針對實現(xiàn)”,頓時有種發(fā)現(xiàn)新?陸的感覺。此時,感覺SOLID?點都不“Solid”,反?很虛。設(shè)計模式才是指導開發(fā)的王道,策略模式也是我最常?的模式之?(可能它最簡單吧)。
后來遇到更復(fù)雜的情況:不同的條件下,需要使?不同的“策略組”(算法組);并且隨著狀態(tài)的流轉(zhuǎn),“策略組”也跟著變化。使?“狀態(tài)模式”,問題迎刃?解。此時的我認為,設(shè)計模式是指導設(shè)計的另?套原則,是對SOLID原則的拓展和應(yīng)?,是對于模糊的原則做了?次詳細的解釋說明,?SOLID本身很難指導開發(fā)設(shè)計。

問題一:狀態(tài)模式中的壞味道
直到一次Code Review時,同事提出了以下疑問:
“雖然增加?個新的狀態(tài)(State)只需改變很少已有代碼,但是如果增加?個新動作(action),是不是所有狀態(tài)?類都實現(xiàn)這個動作?這樣就會把已有的State?類全部更改?遍。 ”
“如果有必要,是的。但?前這個動作只有在StateX下才會真正地使Context的狀態(tài)發(fā)?改變。我會在State中?持默認的實現(xiàn),只有ConcreteStateX才會完成狀態(tài)變化的邏輯。這樣?來就不需要改動每?個State了。 ”
“如果這個‘新動作’只在某?個具體的State中才?效,那么相當于‘兄弟State’不得不?持這個對??毫?意義的動作,?且基類可能有越來越多類似的默認實現(xiàn),此時會出現(xiàn)‘DivergentChange’這個壞味道。?且當我們使?State基類的時候,我們并不清楚哪些?法在哪個具體的State?類有特殊的實現(xiàn),給讀代碼的?也帶來很多困難。 ”
在此之前,我從未懷疑過設(shè)計模式會遭到挑戰(zhàn),?且對?說的貌似有點道理。
問題二:組合模式違反“單一職責”原則
再來看組合模式,Component承擔了Leaf和Composite兩種職責,明顯違背了“單一職責”原則,以后還要不要用呢?

故事三 拍腦袋決定方案?
經(jīng)歷過以上故事的我,對于設(shè)計上的優(yōu)缺點已經(jīng)有了一定的認識,但是在代碼層面竟然也遇到了問題。
在?次重構(gòu)?作中,遇到了“重復(fù)代碼(Duplicated Code) ”的壞味道,我直接使?“PullUp Method”的?法,將重復(fù)代碼推?了超類;?同事卻認為使?繼承不如使?組合,建議使?“Extra Class”,將重復(fù)代碼抽到?個?關(guān)的類。我倆的?法都能解決問題,但是為了說服對?花了很多時間。后來想了想,對于兩個都能解決問題的?案,我是否還要花時間去爭論?如果兩個?案都?,拍腦袋決定豈不美哉?
故事四 DDD如何指導設(shè)計開發(fā)?
隨著在項?中使?DDD,?認為積累了不少經(jīng)驗。?次在DDD Community的討論中,涉及了聚合,便有了下?的對話:
問題一:聚合和一致性的關(guān)系
“……聚合是為了維護模型對象間的固定業(yè)務(wù)規(guī)則?存在,所以A、 B和C在同?個聚合??。 ”
“等等, A和D之間也有關(guān)系,這種關(guān)系難道不是業(yè)務(wù)規(guī)則嗎?為什么要把D排除在聚合邊界之外? ”

“這?說的‘固定業(yè)務(wù)規(guī)則’是強?致性的, A和D之間的業(yè)務(wù)規(guī)則是‘最終?致性’的。 ”
“‘最終?致性’是不是牽強附會的概念呢?你的意思是, A和D之間的業(yè)務(wù)規(guī)則可以‘不?致’,也就是不固定?你問問業(yè)務(wù)?同意嗎? ”
“我的意思是它們之間可以有‘短暫的不?致’,?如發(fā)郵件的那?刻,并不期望郵件在?秒內(nèi)發(fā)出去,它可能在郵件服務(wù)的隊列?等著呢,只要在規(guī)定的時間內(nèi),?如五分鐘,發(fā)出去即可。所以我們經(jīng)常把‘郵件系統(tǒng)’作為?個獨?的系統(tǒng),故?不會把‘郵件’和‘發(fā)送郵件的對象’作為?個聚合。 ”
“聽起來有道理。但是我也?過業(yè)界很多情況下把‘下單’和‘財務(wù)’做成兩個微服務(wù)系統(tǒng)的,所以‘訂單’和‘賬單’肯定不屬于同?個聚合。按照上?的說法,‘訂單’和‘賬單’之間肯定沒有強?致性的業(yè)務(wù)規(guī)則。但是你點外賣的時候,難道不是付款成功之后才會告訴你下單成功嗎?它既不會讓你等五分鐘,也不會沒收到錢就給你準備飯菜。這種規(guī)則難道不是你所謂的‘強?致性’? ”
討論到這個地方,通常會以“具體問題具體分析”結(jié)尾,或者還沒結(jié)尾就轉(zhuǎn)到了另一個相關(guān)話題:
問題二:實體只有一部分需要聚合維護固定規(guī)則
“……前?說到,聚合維系了內(nèi)部對象的固定規(guī)則,所以操作聚合內(nèi)的對象要通過聚合根。但是聚合內(nèi)某些實體的狀態(tài)更新通過聚合根操作效率太低……”
“不通過聚合根操作會破壞業(yè)務(wù)規(guī)則吧? ”
“打個??,在訂單這個聚合?,直接更改某個訂單項的價格可能會破壞訂單的業(yè)務(wù)規(guī)則,?如‘總價限額’。但是更改訂單項的備注并不會破壞任何規(guī)則。如果還有更多類似‘更改備注’這樣的操作,都要通過聚合根這個‘代理’完成。這跟重構(gòu)中的壞味道‘中間?(Middle Man) ’有點像。 ”
“那能不能把訂單項中‘沒有固定規(guī)則’的部分和‘有固定規(guī)則’的部分分開,做成兩個實體對象呢? ”
“?先,在當前上下??,訂單項是?個?常明確的概念,分開后怎樣對應(yīng)業(yè)務(wù)概念呢?其次,如果真的分開,這個新的實體對象?命周期的維護也需要成本。 ”

討論到此處,又陷入了僵局。但討論一旦發(fā)散起來,根本剎不住車。
問題三:實體只有某個生命周期需要聚合維護固定規(guī)則
“上?討論的是‘實體只有?部分需要聚合維護規(guī)則’。我現(xiàn)在遇到的問題是‘實體只有某個?命周期需要聚合維護固定規(guī)則’。這種情況下,相當于實體的其他?命周期的操作也要受限于聚合。 ”
“說來聽聽? ”
“還是拿‘訂單’舉例。在創(chuàng)建訂單的時候,所有訂單項之間才會維護固定規(guī)則,?如‘總價限額’。?旦創(chuàng)建完畢,業(yè)務(wù)規(guī)定不能更改訂單項的價格,也就不再需要聚合維護任何固定規(guī)則了?!额I(lǐng)域驅(qū)動設(shè)計》并沒有給出這種情況下對應(yīng)的明確答案,如果按照書中對聚合處理,同樣也會遇到前?討論的問題。 ”

諸如此類的討論還有很多,?中也有??模糊的答案,但是也產(chǎn)?了更多的疑問——聚合是不是?個業(yè)務(wù)概念?它是?個令?隨意打扮的,還是?個客觀存在的個體?如果“令?隨意打扮”,那該如何打扮呢?如果客觀存在,該如何才能準確地找到它呢?在DDD中有?常多的概念,它們?直在討論中存在爭議,那么DDD該如何指導設(shè)計和開發(fā)呢?
設(shè)計中的妥協(xié)
不知道各位讀者是否也遇到過上?類似的問題。每當讀到?個“新”的“設(shè)計?法”的時候,總免不了“得矣得矣”?沾沾?喜;?在實踐中,很難得到?個完美的解決?案。
我也?直在思考,到底什么樣的架構(gòu)才是“完美”的?
后來我在《架構(gòu)整潔之道》里面找到了答案:“軟件架構(gòu)的終極?標是,?最?的??成本來滿?構(gòu)建和維護該系統(tǒng)的需求”(以下簡稱“最???成本原則”)。如果我們把這句話當作架構(gòu)的?標,那么很多設(shè)計問題都會迎刃?解了。
在“故事二”的“問題一”中,新的解決方案可以是“把每種狀態(tài)的動作處理做成可配置的,配置項通過多個策略模式完成”。這樣只拓展新增加的aciton策略,相同的策略只需要相同的配置即可,并且還將state的控制權(quán)還給了Context。

但是這種方案是不是“最小人力成本原則”呢?未必!狀態(tài)模式作為成熟的設(shè)計模式,更容易被開發(fā)者理解和應(yīng)用。而且我們還要面對團隊開發(fā)人員的技術(shù)水平問題:當前的開發(fā)團隊能否理解新的解決方案呢?我們需要花多大代價普及這個方案呢?這個方案是否可以作為一個新的模式在團隊推廣呢?
在“故事二”的“問題二”中,組合模式也是經(jīng)過千千萬萬開發(fā)人員采用并驗證過的,它是處理樹型結(jié)構(gòu)的典型方案。雖然它違背了某些原則,但是如果“妥協(xié)”一下,它還是非常好用的工具。相反,如果相似的問題不采用組合模式,新的方案能否滿足組合模式的所有優(yōu)點呢?
在“故事四”的“問題一”中,雖然D和A、B、C之間有業(yè)務(wù)規(guī)則,但是如果放在一個聚合里面維護,會不會因為聚合內(nèi)過于復(fù)雜而無法滿足“最小人力成本原則”呢?如果是,我們只能通過“妥協(xié)”,把相對聯(lián)系不太緊密的D排除在聚合之外。其他問題的分析也是同理。
討論到這里,所有的問題還是沒有確切的答案,一切都是依賴具體的“環(huán)境”做出的妥協(xié)。
如果我們把“最小人力成本原則”來作為設(shè)計原則或者方法,那什么是“最小”呢?如何衡量最小呢?難道軟件的設(shè)計原則真的是“具體問題具體分析”?而且它聽起來更像是一個條管理原則,那我們學習SOLID和DDD還有什么用?
原則和模式
三年前,我以DDD專家身份去客戶現(xiàn)場,幾個同事在討論具體概念的時候發(fā)生了分歧。這時,一位資深的同事問我們:“你認為架構(gòu)的設(shè)計原則是什么?”我思考了一下:“高內(nèi)聚,低耦合”。雖然當時得到了大佬的認同,但我并沒有對自己的答案有多少信心。
如果我們把“高內(nèi)聚,低耦合”作為原則對前面的問題進行分析,似乎也能得到類似的答案,畢竟“內(nèi)聚性高,耦合性低”確實能降低構(gòu)建和維護系統(tǒng)需求的成本。但是它的缺點也同樣存在——到底內(nèi)聚性達到什么程度才算“高內(nèi)聚”呢?畢竟這兩個指標沒法量化。如果我們在進行OO建模,此時我們會發(fā)現(xiàn),SOLID原則會告訴你怎樣做到“高內(nèi)聚,低耦合”。單一職責原則要求“一個類或者模塊應(yīng)該有且只有一個改變的原因”,其實就是對“高內(nèi)聚,低耦合”的一個應(yīng)用。
所以,我們可以把之前提到的所有“原則”都作為設(shè)計的指導,只不過在不同層級,不同粒度上會有不同。
那么,設(shè)計模式和“聚合”也是設(shè)計原則嗎?在我們的討論中,它們不是原則,而是模式。
模式可以理解為以原則為指導,針對一類問題提出的可復(fù)用的解決方案。設(shè)計模式很多情況下都是印證了SOLID原則。既然“針對一類問題”,它必然有嚴格的使用條件。
在“故事三”中,針對“消除重復(fù)代碼”的原則,提出了多個“模式”——“Pull Up Method”和“Extract Class”?!吨貥?gòu):改善既有代碼的設(shè)計》也給出了使用條件——如果兩個類不相干,那么使用“Extract Class”;如果兩個類有很多共性,或者本來就屬于同一個繼承結(jié)構(gòu),那么使用“Pull Up Method”。所以當我們搞清楚模式的使用條件時,就不用拍腦袋決定了。
在“故事四”的“問題三”中,聚合模式似乎不能滿足“實體只在某個生命周期才需要一個組合結(jié)構(gòu)維護固定規(guī)則,而其他生命周期和這個組合結(jié)構(gòu)解耦”的問題。那么聚合模式是錯誤的嗎?并不是,它仍然滿足了大多數(shù)情況下的設(shè)計要求。很顯然,我們此時的特殊需求,超越了聚合模式的使用范圍。
既然模式不再適用,那我們就通過原則來指導設(shè)計。
首先,根據(jù)“最小人力成本原則”,使用聚合模式和“疑似中間人(Middle Man)壞味道”兩者之間,哪個給團隊帶來的成本最小呢?如果使用聚合代價更小,那我們欣然接受聚合模式;反之,考慮“聚合”的設(shè)計原則。
在《領(lǐng)域驅(qū)動設(shè)計:軟件核心復(fù)雜性應(yīng)對之道》里討論聚合的時候反復(fù)提到“局部和整體”和“一致性”,可以認為這是聚合模式的理論來源,也就是設(shè)計聚合的原則。事實上,它把復(fù)雜的變化封裝在了一起,也符合我們說的“高內(nèi)聚,低耦合”的原則。如果我們放棄聚合模式,那么就用前面說的原則進行重新設(shè)計。這樣無論設(shè)計出的怎樣的模型,他都是符合DDD設(shè)計思想的。如果這種模型能解決一類問題,它甚至可以命名為新的模式。所以,一旦分清了原則和模式之間的關(guān)系,更利于我們做設(shè)計工作。
因此:
軟件的設(shè)計原則是分層級的:
- 高層原則是抽象的,難以指導設(shè)計工作;
- 低層原則是對高層原則在某個方面的細化,但它不能違背高層原則。
模式以原則為指導,針對一類問題提出的可復(fù)用的解決方案,并且有明確的使用條件。
做設(shè)計時,優(yōu)先以滿足條件的模式為指導,當模式無法滿足設(shè)計時,以對應(yīng)層次的原則作為指導。當?shù)蛯釉瓌t無法指導設(shè)計時,向高層依次尋找原則。當新的設(shè)計方案能解決某一類問題時,它可能就是一種新的模式。
由上面的總結(jié)可以看出,模式作為設(shè)計的武器,當武器庫中的武器都不能滿足設(shè)計要求時,要么選擇“妥協(xié)”,找一個最趁手的武器用起來,要么根據(jù)“原則”對武器進行升級打造,豐富武器庫。(古代著名軍事家戚繼光在抗倭時,以長矛為基礎(chǔ)發(fā)明狼筅,以克制倭刀。)
回到“高內(nèi)聚,低耦合”
如果現(xiàn)在有人問我:“你的設(shè)計原則是什么?”我的回答可能是“高內(nèi)聚,低耦合”,也可能是“封裝、繼承和多態(tài)”,但答案不再唯一。如果我們此刻在討論“劃分限界上下文”的原則,上面的答案就跟下面的問答類似:
問:“為什么鐵球會落地?”
答:“因為萬有引力?!?/p>
在物理界中,所有物理學家的終極夢想是發(fā)現(xiàn)一個宇宙通用的公式(就像“萬有引力”能夠解釋“重力”一樣),這個公式能夠解釋一切物理現(xiàn)象。且不論這個公式是否存在,即便存在,應(yīng)該也是極其復(fù)雜的。在我們的設(shè)計過程中,不必用最高層的原則指導一切,那樣指導具體設(shè)計時就會變得模糊。
推薦閱讀
- 如何實現(xiàn)系統(tǒng)解耦
- 如何理解SOLID原則?
- 代碼的簡單設(shè)計五原則
- 用DDD指導微服務(wù)拆分
- DDD的戰(zhàn)略設(shè)計和戰(zhàn)術(shù)設(shè)計
- 寫了這么多年代碼,你真的了解設(shè)計模式么?
- 《Head First設(shè)計模式》
- 《重構(gòu):改善既有代碼的設(shè)計》
- 《領(lǐng)域驅(qū)動設(shè)計:軟件核心復(fù)雜性應(yīng)對之道》
- 《架構(gòu)整潔之道》
文/Thoughtworks 王萬徳
原文鏈接:https://insights.thoughtworks.cn/architecture-design-principles-patterns/