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