拋開(kāi)性能、并發(fā)、一致性等技術(shù)因素,好的業(yè)務(wù)代碼應(yīng)當(dāng)如一篇顯淺易懂的業(yè)務(wù)敘實(shí)文章,滿足以下幾個(gè)基本條件:
因此,好代碼如同好文章,它應(yīng)該是飽含業(yè)務(wù)語(yǔ)義(詞要達(dá)意)、具有自明性和可讀性(結(jié)構(gòu)清晰),能夠顯性化表達(dá)業(yè)務(wù)意圖(緊扣主題),讓人賞心悅目。
好的代碼,從好的命名開(kāi)始,做到名副其實(shí)。
變量名是名詞,要正確和清晰地描述業(yè)務(wù)語(yǔ)義,如果一個(gè)變量需要通過(guò)注釋補(bǔ)充說(shuō)明,那可能就是沒(méi)取好變量名。
變量命名的關(guān)鍵點(diǎn):
1、詞要達(dá)意:避免無(wú)業(yè)務(wù)語(yǔ)義的命名,如:list、val、a…;
2、語(yǔ)境范圍:避免小范圍詞套大范圍數(shù)據(jù),反之亦然,不使用過(guò)于寬泛的名詞。
3、名詞復(fù)數(shù):統(tǒng)一風(fēng)格,加s或List尾綴,變量名建議使用s尾綴,函數(shù)名建議使用List尾綴。
4、后置限定詞:限定詞是對(duì)前面變量名的修飾,可以描述名詞的作用范圍屬性,例如:
Bad case:
Good case:
函數(shù)命名要體現(xiàn)做什么,而不是怎么做,要清楚表達(dá)出操作意圖和業(yè)務(wù)語(yǔ)義。
函數(shù)命名的關(guān)鍵點(diǎn):
Bad Case:
Good Case:
類是面向?qū)ο笾凶钪匾母拍?,是一組關(guān)聯(lián)數(shù)據(jù)的相關(guān)操作的封裝,通??梢园杨惙譃閮煞N:
函數(shù)命名的關(guān)鍵點(diǎn):
包(package)是一組強(qiáng)關(guān)聯(lián)(內(nèi)聚)的類的集合,起分類收納和命名空間的作用。
實(shí)際工程中,常見(jiàn)的分類維度主要是兩種,按功能性或業(yè)務(wù)域分類。
同一層級(jí)的包,要嚴(yán)格保持分類維度的一致性,要么先按業(yè)務(wù)域分類,再按功能性分類;要么就先按功能性分類,再按業(yè)務(wù)域分類。
有時(shí)候,優(yōu)雅的實(shí)現(xiàn)僅僅是一個(gè)函數(shù),不是一個(gè)類,不是一個(gè)框架,只是一個(gè)函數(shù)。 —— John Carmack
遵循金字塔原則,把函數(shù)層層遞進(jìn)的調(diào)用,理解成結(jié)論先行,自上而下的表達(dá)過(guò)程。
同層函數(shù)是對(duì)上一層的支撐,同層間要符合MECE法則,應(yīng)描述和處理同一邏輯范疇的事情,高層抽象和底層細(xì)節(jié)不能雜糅在一起,否則會(huì)變得凌亂和難以理解。
MECE是(Mutually Exclusive Collectively Exhaustive)的縮寫,指的是“相互獨(dú)立,完全窮盡”的分類原則。通過(guò)MECE方法對(duì)問(wèn)題進(jìn)行分類,能做到清晰準(zhǔn)確,從而容易找到答案。
分包的建議:
例如:
軟件設(shè)計(jì)的目標(biāo)是高內(nèi)聚、低耦合。如果代碼是高耦合和低內(nèi)聚的,就會(huì)出現(xiàn)修改一個(gè)邏輯,多處代碼要修改,可能影響到多個(gè)業(yè)務(wù)鏈路,增加了出bug的業(yè)務(wù)風(fēng)險(xiǎn),同時(shí)擴(kuò)大了測(cè)試回歸的范圍,導(dǎo)致研發(fā)成本增加。
耦合和內(nèi)聚,是我們常掛在嘴邊的話,但是大家經(jīng)常說(shuō)不太清楚,講不太明白,很難衡量:
耦合是描述模塊(系統(tǒng)/模塊/類/函數(shù))之間相互聯(lián)系(控制/調(diào)用/數(shù)據(jù)傳遞)緊密程度的一種度量。
如果兩個(gè)模塊之間沒(méi)有直接關(guān)系,它們之間的聯(lián)系完全是通過(guò)主模塊控制調(diào)用來(lái)實(shí)現(xiàn)的,這就是非直接耦合,這種耦合的模塊獨(dú)立性最強(qiáng)。
如果一個(gè)模塊訪問(wèn)另一個(gè)模塊時(shí),彼此之間是通過(guò)數(shù)據(jù)參數(shù)(不是控制參數(shù)、公共數(shù)據(jù)結(jié)構(gòu)或外部變量)來(lái)交換輸入、輸出信息的,則稱這種耦合為數(shù)據(jù)耦合,它是較好的耦合形式。
當(dāng)模塊之間使用復(fù)合數(shù)據(jù)結(jié)構(gòu)進(jìn)行通信時(shí),就會(huì)發(fā)生印記耦合。
復(fù)合數(shù)據(jù)結(jié)構(gòu)可以是數(shù)組、類、結(jié)構(gòu)體、聯(lián)合體等的引用,通過(guò)復(fù)合數(shù)據(jù)結(jié)構(gòu)在模塊之間傳遞的參數(shù),可能會(huì)或不會(huì)被接收模塊完全使用。
印記耦合優(yōu)點(diǎn):把模塊A的引用一把傳遞給模塊B,模塊B只需要接受少量參數(shù),接口說(shuō)明簡(jiǎn)單。
印記耦合缺點(diǎn):
印記耦合優(yōu)化:增加入?yún)?shù)類型,進(jìn)傳入模塊需要的必要數(shù)據(jù),如下:
如果一個(gè)模塊通過(guò)傳送開(kāi)關(guān)、標(biāo)志等控制信息,明顯地控制選擇另一模塊的功能,就是控制耦合。
外部耦合,是指多個(gè)模塊同時(shí)依賴同一個(gè)外部因素(IO設(shè)備/文件/協(xié)議/DB等),如上圖所示:
外部耦合與與外部設(shè)備的通信有關(guān),而不是與公共數(shù)據(jù)或數(shù)據(jù)流有關(guān)。
一個(gè)模塊對(duì)外部數(shù)據(jù)或通信協(xié)議所做的任何更改都會(huì)影響其他模塊,可以通過(guò)增加中間模塊隔離外部變化來(lái)降低耦合度,如下:
共用耦合是指不同的模塊共享全局?jǐn)?shù)據(jù)的信息(全局?jǐn)?shù)據(jù)結(jié)構(gòu)、共享的通信區(qū)、內(nèi)存的公共覆蓋區(qū))。
共用耦合的問(wèn)題:
內(nèi)容耦合在低級(jí)語(yǔ)言(匯編)中出現(xiàn),高級(jí)語(yǔ)言從設(shè)計(jì)上已避免出現(xiàn)內(nèi)容耦合。
如果發(fā)生下列情形,兩個(gè)模塊之間就發(fā)生了內(nèi)容耦合:
內(nèi)聚,是描述一個(gè)模塊內(nèi)各元素彼此結(jié)合的緊密程度,是從功能角度來(lái)度量模塊內(nèi)的聯(lián)系。
通常,解決了耦合的問(wèn)題,就解決了內(nèi)聚的問(wèn)題,反之亦然。
偶然內(nèi)聚,一個(gè)模塊內(nèi)的各元素之間沒(méi)有任何聯(lián)系,僅是恰好放在同一個(gè)模塊內(nèi),業(yè)務(wù)的“Util/Helper”類有大量例子。
邏輯內(nèi)聚,把幾種相關(guān)的功能組合在一起,由調(diào)用方傳入的參數(shù)來(lái)確定具體執(zhí)行哪一種功能。
邏輯內(nèi)聚是一種“低內(nèi)聚”,某程度上對(duì)應(yīng)了“控制耦合”,它把內(nèi)部的邏輯處理暴露給了接口之外,當(dāng)內(nèi)部邏輯發(fā)生變更時(shí),原本無(wú)辜的調(diào)用方也會(huì)受牽連改動(dòng)。
時(shí)間內(nèi)聚,指一個(gè)模塊內(nèi)的組件除了在同一時(shí)間都會(huì)被執(zhí)行外,相互之間沒(méi)有任何關(guān)聯(lián)。
過(guò)程內(nèi)聚,指一個(gè)模塊內(nèi)的組件以特定次序被執(zhí)行,但相互之間沒(méi)有數(shù)據(jù)傳遞。
通信內(nèi)聚,指一個(gè)模塊內(nèi)的組件以特定次序被執(zhí)行,且相互之間傳遞和操作相同的數(shù)據(jù)。
順序內(nèi)聚,指一個(gè)模塊內(nèi)的元素以特定次序被執(zhí)行,且上一步的輸出被下一元素所依賴。
功能內(nèi)聚,指一個(gè)模塊內(nèi)所有組件屬于一個(gè)整體,完成同一個(gè)不可切分的功能,彼此缺一不可。
設(shè)計(jì)原則,是指導(dǎo)我們?nèi)绾卧O(shè)計(jì)出低耦合、高內(nèi)聚的代碼,讓代碼能夠更好的應(yīng)對(duì)變化,從而降本提效。
設(shè)計(jì)原則的關(guān)鍵,是從使用方的角度看提供方的設(shè)計(jì),一句話概括就是:請(qǐng)不要要我知道太多,你可以改,但請(qǐng)不要影響我。
定義:一個(gè)函數(shù)/類只能因?yàn)橐粋€(gè)理由被修改。
單一職責(zé)原則,是所有原則中看起來(lái)最容易理解的,但是真正做到并不簡(jiǎn)單。因?yàn)樽裱@一原則最關(guān)鍵是職責(zé)的劃分。
職責(zé)的劃分至少要回答兩個(gè)基本問(wèn)題:
且不說(shuō)寫代碼,工作中我們也會(huì)出現(xiàn)人人不管或相爭(zhēng)的重疊地帶,劃分清楚職責(zé)看起容易,實(shí)際很難。
定義:對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉(不修改代碼就可以增加新功能)。
要理解開(kāi)閉原則,關(guān)鍵是要理解定義中隱含著的兩個(gè)主語(yǔ),“使用方”和“提供方”,即:
提供方可以修改,增加新的功能特性,但是使用方不需要被修改,即可享用新的功能特征。
開(kāi)閉原則廣泛的理解,可以指導(dǎo)類、模塊、系統(tǒng)的設(shè)計(jì),滿足該原則的核心設(shè)計(jì)方法是:通過(guò)協(xié)議(接口)交互。
定義:所有引用父類的地方,必須能透明的使用它的子類對(duì)象,指導(dǎo)類繼承的設(shè)計(jì)。
面向?qū)ο蟮睦^承特性,一方面,子類可以擁有父類的屬性和方法,提高了代碼的復(fù)用性;另一方面,繼承是有入侵性的,父類對(duì)子類有約束,子類必須擁有父類全部的屬性和方法,修改父類會(huì)影響子類,增加了耦合性。
里氏替換原則是對(duì)繼承進(jìn)行了約束,體現(xiàn)在以下方面:
定義:高層模塊不應(yīng)該依賴低層模塊,兩者都應(yīng)該依賴其抽象;抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象,目的是降低層與層之間的耦合。
從倒置來(lái)看,該原則可以有更泛化的理解:
舉個(gè)購(gòu)物車的例子:
定義:客戶端不應(yīng)該被強(qiáng)迫去依賴它并不需要的接口。
理解接口隔離原則,需要拿單一職責(zé)的原則做對(duì)比。細(xì)品一下,如果一個(gè)接口滿足了單一職責(zé),是否就也就滿足接口隔離原則?
簡(jiǎn)單來(lái)講,接口隔離原則解決的問(wèn)題是,當(dāng)某些類本身或面向使用方不滿足職責(zé)單一原則時(shí),客戶端不應(yīng)該直接使用它們,而是通過(guò)增加接口類,通過(guò)它隱藏客戶端不需要感知到的部分。
編程范式,本質(zhì)是一種思維方式,而和具體語(yǔ)言沒(méi)關(guān)系。
用C語(yǔ)言可以寫出面向?qū)ο蟮某绦?,用Java語(yǔ)言可以寫出面向過(guò)程的程序。而不爭(zhēng)的現(xiàn)實(shí)是,我們大部分人是在用java寫面向過(guò)程的代碼。
例如下面代碼,它是如何用面向過(guò)程語(yǔ)言實(shí)現(xiàn)封裝、繼承、多態(tài)的?
備注:以上代碼來(lái)自開(kāi)源libevent庫(kù)
最早使用機(jī)器和匯編語(yǔ)言編程,是編排好一堆命令讓機(jī)器逐條執(zhí)行,為了控制一些跳躍的流程(如if/for/continue/break),就會(huì)用到類似goto的語(yǔ)句,讓程序直接跳轉(zhuǎn)到希望執(zhí)行的指令位置,這樣程序員就擁有了直接轉(zhuǎn)移程序控制權(quán)的能力。goto的無(wú)條件轉(zhuǎn)移,使得程序的控制流難于追蹤,程序難以修改和維護(hù)。
后來(lái)大家出了一套流程結(jié)構(gòu)化的定律:任何程序都可以用順序、選擇、循環(huán)三種基本控制結(jié)構(gòu)來(lái)表示。
因此,結(jié)構(gòu)化編程的本質(zhì),是對(duì)程序控制權(quán)的直接轉(zhuǎn)移進(jìn)行了規(guī)范和限制。
結(jié)構(gòu)化編程思維,比較靠近機(jī)器運(yùn)行的思維,當(dāng)程序越來(lái)越復(fù)雜的時(shí)候,大家發(fā)現(xiàn)簡(jiǎn)單靠結(jié)構(gòu)化思維編程,很難構(gòu)建起一個(gè)龐大的應(yīng)用。而在編碼過(guò)程中,大家不知不覺(jué)的把一些數(shù)據(jù)和邏輯封裝了起來(lái),形成一個(gè)個(gè)可復(fù)用的組件。慢慢大家出了一套符合人類理解客觀世界的編程范式:利用事物的信息建模概念,如實(shí)體、關(guān)系、屬性等,同時(shí)運(yùn)用封裝、繼承、多態(tài)等機(jī)制來(lái)構(gòu)造模擬現(xiàn)實(shí)系統(tǒng)的方法。
封裝、繼承、多態(tài)是面向?qū)ο蟮娜筇卣?,三者的關(guān)系是層層遞進(jìn)的,而多態(tài)實(shí)際是規(guī)范了程序控制權(quán)的間接轉(zhuǎn)移,在面向?qū)ο缶幊讨?,大家是通過(guò)函數(shù)指針來(lái)解耦不同組件的函數(shù)實(shí)現(xiàn),這種方式需要工程師嚴(yán)格遵守約定初始化函數(shù)指針,是非常脆弱的。
因此,面向?qū)ο缶幊痰谋举|(zhì),是規(guī)范了數(shù)據(jù)和行為的封裝,同時(shí)限制了程序控制權(quán)的間接轉(zhuǎn)移。
函數(shù)式思維,是一種數(shù)學(xué)思維,把一個(gè)問(wèn)題分解為一系列函數(shù)。函數(shù)式編程有多種定義,但是從根本上來(lái)看,它的核心是“純函數(shù)”和“引用透明”:
若要做到以上兩點(diǎn),就需要對(duì)賦值進(jìn)行限制,即變量一旦初始化就不可以再修改。
因此,函數(shù)式編程的本質(zhì),是規(guī)范了函數(shù)(一等公民/高階函數(shù)/聲明式/閉包等),同時(shí)限制了賦值行為。
編程范式的本質(zhì),更多是告訴我們不能做什么,并且通過(guò)規(guī)范來(lái)約束我們的行為。
靈魂拷問(wèn)一下:
我當(dāng)前表淺的理解是:
三種編程范式?jīng)]有好壞之分,核心是思維方式的區(qū)別,針對(duì)不同的問(wèn)題和場(chǎng)景,如何選擇適當(dāng)?shù)姆绞絹?lái)思考和解決問(wèn)題,才是我們理解它們的關(guān)鍵。
表模式關(guān)注的數(shù)據(jù)庫(kù)的表,它先考慮數(shù)據(jù)庫(kù)表需要管理,然后添加對(duì)數(shù)據(jù)增刪改查的操作。封裝是面向?qū)ο蟮年P(guān)鍵特征之一,把數(shù)據(jù)和操作數(shù)據(jù)的行為綁定在一起,擁有一個(gè)標(biāo)識(shí)符(類)來(lái)表示它兩的集合,而表模式允許你把數(shù)據(jù)和行為放在一起,但是它沒(méi)有一個(gè)標(biāo)識(shí)符來(lái)標(biāo)出它所代表的主體。
這種模式在PC時(shí)代很盛行,例如VB和.net等桌面應(yīng)用開(kāi)發(fā)框架上,但是在JAVA服務(wù)應(yīng)用中也被我發(fā)現(xiàn)了,如下:
腳本,是指表演戲劇、拍攝電影等所依據(jù)的底本又或者書稿的底本。腳本可以說(shuō)是故事的發(fā)展大綱,用以確定故事的發(fā)展方向。
事務(wù)腳本模式,關(guān)注點(diǎn)是事務(wù)的流程和步驟,是對(duì)事務(wù)流程和步驟的編排,是一種面向過(guò)程的組織和表達(dá)形式。
按照事務(wù)腳本模式編程,可以不需要任何面向?qū)ο蟮脑O(shè)計(jì),其中任何邏輯都可以通過(guò)if/else/while等流程控制元素來(lái)表達(dá)。
事務(wù)腳本模式的優(yōu)點(diǎn)是,門檻低容易上手,也符合人的直線直覺(jué)思維;它的缺點(diǎn)是,當(dāng)業(yè)務(wù)邏輯復(fù)雜是,事務(wù)方法會(huì)快速膨脹,因?yàn)闃I(yè)務(wù)屬性不明確和缺乏抽象,不好復(fù)用和擴(kuò)展。該模式在服務(wù)端應(yīng)用中很常見(jiàn),從MVC時(shí)代開(kāi)始,一般通過(guò)controller組織事務(wù)流程,常見(jiàn)的分層結(jié)構(gòu)如下:
領(lǐng)域設(shè)計(jì)模式,是通過(guò)分析和發(fā)掘業(yè)務(wù)領(lǐng)域的概念,從中提煉和設(shè)計(jì)出具有數(shù)據(jù)和行為的對(duì)象模型(類),并建立模型之間的關(guān)系。
領(lǐng)域設(shè)計(jì)模式,需要建立一個(gè)完整的由對(duì)象模型組成的層,來(lái)對(duì)目標(biāo)業(yè)務(wù)領(lǐng)域建模。業(yè)務(wù)是經(jīng)常變化的,通常有會(huì)通過(guò)分層的模式,讓領(lǐng)域模型和系統(tǒng)其他部分保持最小的依賴。
至此,你會(huì)發(fā)現(xiàn)領(lǐng)域設(shè)計(jì)是DDD的底層思想,是面向?qū)ο蟮膶?shí)踐,更多請(qǐng)查閱“對(duì)象建?!焙汀邦I(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)”相關(guān)的材料和數(shù)據(jù),這里不做展開(kāi)。
不同的應(yīng)用范式,是隨著軟件復(fù)雜度逐步提升演進(jìn)出來(lái)的,不同模式面對(duì)和解決不同復(fù)雜度的問(wèn)題,相互之間沒(méi)有好壞之分。當(dāng)問(wèn)題比較簡(jiǎn)單時(shí),使用事務(wù)腳本模式足夠應(yīng)付,反倒使用領(lǐng)域設(shè)計(jì)就過(guò)度設(shè)計(jì),增加了不必要的復(fù)雜度,適得其反。
任何一個(gè)學(xué)科的學(xué)習(xí),都要從基本概念、基本原理、基本方法入手,才能把握住問(wèn)題的實(shí)質(zhì)。
所謂,招式套路可以千變?nèi)f化,扎實(shí)深厚的內(nèi)功卻始終如一。內(nèi)功是基礎(chǔ)和本源的東西,例如耦合和內(nèi)聚,我們都知道低耦合高內(nèi)聚好,但如何衡量代碼的耦合和內(nèi)聚?再如編程范式,我們都在使用面向?qū)ο笳Z(yǔ)言,為什么看到的大多數(shù)是面向過(guò)程的代碼?究其根本,是我們?nèi)菀缀鲆暬A(chǔ)和本源的東西,比如更關(guān)注設(shè)計(jì)模式,更關(guān)注架構(gòu)設(shè)計(jì),但上層的設(shè)計(jì)理念大多數(shù)是來(lái)自基礎(chǔ)和本源的思想指引。
套用道家的一句話:道以明向,法以立本,術(shù)以立策,器以成事。
從代碼的角度來(lái)看:
從代碼的角度來(lái)看它們的關(guān)系:
關(guān)于如何寫好代碼,描述如有不當(dāng)之處,請(qǐng)大家?guī)兔χ刚?。最后,用一句話與大家共勉:萬(wàn)丈高樓平地起,勿在浮沙筑高臺(tái)。
《control-coupling》
《sequential-cohesion》