自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

寫過25W行代碼,3個操作系統(tǒng):我如何做架構設計來降低代碼復雜度?

新聞 前端
本文是作者閱讀John Ousterhout的《A Philosophy of Software Design》之后,結合自己的工作經(jīng)驗,對“降低復雜性”做了詳細總結,希望給讀者朋友們帶來不一樣的思路。

 

一、前言

斯坦福教授、Tcl語言發(fā)明者John Ousterhout 的著作《A Philosophy of Software Design》[1],自出版以來,好評如潮。

John Ousterhout累計寫過25萬行代碼,是3個操作系統(tǒng)的重要貢獻者,這些原則可以視為作者編程經(jīng)驗的總結。

按照IT圖書出版的慣例,如果冠名為“實踐”,書中內(nèi)容關注的是某項技術的細節(jié)和技巧;冠名為“藝術”,內(nèi)容可能是記錄優(yōu)秀作品的設計過程和經(jīng)驗;而冠名為“哲學”,則是一些通用的原則和方法論,這些原則方法論串起來,能夠形成一個體系。正如“知行合一”、“世界是由原子構成的”、“我思故我在”,這些耳熟能詳?shù)木渥幽軌蛞欢ǔ潭壬洗肀澈蟮娜宋锖退枷搿S靡痪湓捀爬ā禔 Philosophy of Software Design》,軟件設計的核心在于降低復雜性 。

本篇文章是圍繞著“降低復雜性”這個主題展開的,很多重要的結論來源于John Ousterhout,筆者覺得很有共鳴,就做了一些相關話題的延伸、補充了一些實例。雖說是“一般原則”,也不意味著是絕對的真理,整理出來,只是為了引發(fā)大家對軟件設計的思考。

二、如何定義復雜性

關于復雜性,尚無統(tǒng)一的定義,從不同的角度可以給出不同的答案。可以用數(shù)量來度量,比如芯片集成的電子器件越多越復雜( 不一定對 );按層次性[2]度量,復雜度在于層次的遞歸性和不可分解性。在信息論中,使用熵來度量信息的不確定性。

John Ousterhout選擇從認知的負擔和開發(fā)工作量的角度來定義軟件的復雜性,并且給出了一個復雜度量公式:

子模塊的復雜度C p 乘以該模塊對應的開發(fā)時間權重值 t p ,累加后得到系統(tǒng)的整體復雜度C。系統(tǒng)整體的復雜度并不簡單等于所有子模塊復雜度的累加,還要考慮開發(fā)維護該模塊所花費的時間在整體時間中的占比( 對應權重值 t p )。也就是說,即使某個模塊非常復雜,如果很少使用或修改,也不會對系統(tǒng)的整體復雜度造成大的影響。

子模塊的復雜度 C p 是一個經(jīng)驗值,它關注幾個現(xiàn)象:

  • 修改擴散 ,修改時有連鎖反應。

  • 認知負擔 ,開發(fā)人員需要多長時間來理解功能模塊。

  • 不可知 ( Unknown Unknowns ),開發(fā)人員在接到任務時,不知道從哪里入手。

造成復雜的原因一般是代碼依賴和晦澀( Obscurity )。其中,依賴是指某部分代碼不能被獨立地修改和理解,必定會牽涉到其他代碼。代碼晦澀,是指從代碼中難以找到重要信息。

三、解決復雜性的一般原則

首先,互聯(lián)網(wǎng)行業(yè)的軟件系統(tǒng),很難一開始就做出完美的設計,通過一個個功能模塊衍生迭代,系統(tǒng)才會逐步成型。對于現(xiàn)存的系統(tǒng),也很難通過一個大動作,一勞永逸地解決所有問題。系統(tǒng)設計是需要持續(xù)投入的工作,通過細節(jié)的積累,最終得到一個完善的系統(tǒng)。因此,好的設計是日拱一卒的結果,在日常工作中要重視設計和細節(jié)的改進。

其次,專業(yè)化分工和代碼復用促成了軟件生產(chǎn)率的提升。比如硬件工程師、軟件工程師( 底層、應用、不同編程語言 )可以在無需了解對方技術背景的情況下進行合作開發(fā);同一領域服務可以支撐不同的上層應用邏輯等等。其背后的思想,無非是通過將系統(tǒng)分成若干個水平層、明確每一層的角色和分工,來降低單個層次的復雜性。同時,每個層次只要給相鄰層提供一致的接口,可以用不同的方法實現(xiàn),這就為軟件重用提供了支持。分層是解決復雜性問題的重要原則。

第三,與分層類似,分模塊是從垂直方向來分解系統(tǒng)。分模塊最常見的應用場景,是如今廣泛流行的微服務。分模塊降低了單模塊的復雜性,但是也會引入新的復雜性,例如模塊與模塊的交互,后面的章節(jié)會討論這個問題。這里,我們將第三個原則確定為分模塊。

最后,代碼能夠描述程序的工作流程和結果,卻很難描述開發(fā)人員的思路,而注釋和文檔可以。此外,通過注釋和文檔,開發(fā)人員在不閱讀實現(xiàn)代碼的情況下,就可以理解程序的功能,注釋間接促成了代碼抽象。好的注釋能夠幫助解決軟件復雜性問題,尤其是認知負擔和不可知問題( Unknown Unknowns )。

四、解決復雜性之日拱一卒

4.1 拒絕戰(zhàn)術編程

戰(zhàn)術編程致力于完成任務,新增加特性或者修改Bug時,能解決問題就好。這種工作方式,會逐漸增加系統(tǒng)的復雜性。如果系統(tǒng)復雜到難以維護時,再去重構會花費大量的時間,很可能會影響新功能的迭代。

戰(zhàn)略編程,是指重視設計并愿意投入時間,短時間內(nèi)可能會降低工作效率,但是長期看,會增加系統(tǒng)的可維護性和迭代效率。

設計系統(tǒng)時,很難在開始階段就面面俱到。好的設計應該體現(xiàn)在一個個小的模塊上,修改Bug時,也應該抱著設計新系統(tǒng)的心態(tài),完工后讓人感覺不到“修補”的痕跡。 經(jīng)過累積,最終形成一個完善的系統(tǒng)。從長期看,對于中大型的系統(tǒng),將日常開發(fā)時間的10%-15%用于設計是值得的。有一種觀點認為,創(chuàng)業(yè)公司需要追求業(yè)務迭代速度和節(jié)省成本,可以容忍糟糕的設計,這是用錯誤的方法去追求正確的目標。降低開發(fā)成本最有效的方式是雇傭優(yōu)秀的工程師,而不是在設計上做妥協(xié)。

4.2 設計兩次

為一個類、模塊或者系統(tǒng)的設計提供兩套或更多方案,有利于我們找到最佳設計。以我們?nèi)粘5募夹g方案設計為例,技術方案本質(zhì)上需要回答兩個問題,其一,為什么該方案可行?其二,在已有資源限制下,為什么該方案是最優(yōu)的?為了回答第一個問題,我們需要在技術方案里補充架構圖、接口設計和時間人力估算。而要回答第二個問題,需要我們在關鍵點或爭議處提供二到三種方案,并給出建議方案,這樣才有說服力。

通常情況下,我們會花費很多的時間準備第一個問題,而忽略第二個問題。其實,回答好第二個問題很重要,大型軟件的設計已經(jīng)復雜到?jīng)]人能夠一次就想到最佳方案,一個僅僅“可行”的方案,可能會給系統(tǒng)增加額外的復雜性。對聰明人來說,接受這點更困難,因為他們習慣于“一次搞定問題”。但是聰明人遲早也會碰到自己的瓶頸,在低水平問題上徘徊,不如花費更多時間思考,去解決真正有挑戰(zhàn)性的問題。

五、解決復雜性之分層

5.1 層次和抽象

軟件系統(tǒng)由不同的層次組成,層次之間通過接口來交互。在嚴格分層的系統(tǒng)里,內(nèi)部的層只對相鄰的層次可見,這樣就可以將一個復雜問題分解成增量步驟序列。由于每一層最多影響兩層,也給維護帶來了很大的便利。分層系統(tǒng)最有名的實例是TCP/IP網(wǎng)絡模型。

在分層系統(tǒng)里,每一層應該具有不同的抽象。TCP/IP模型中,應用層的抽象是用戶接口和交互;傳輸層的抽象是端口和應用之間的數(shù)據(jù)傳輸;網(wǎng)絡層的抽象是基于IP的尋址和數(shù)據(jù)傳輸;鏈路層的抽象是適配和虛擬硬件設備。如果不同的層具有相同的抽象,可能存在層次邊界不清晰的問題。

5.2 復雜性下沉

不應該讓用戶直面系統(tǒng)的復雜性,即便有額外的工作量,開發(fā)人員也應當盡量讓用戶使用起來更簡單。如果一定要在某個層次處理復雜性,這個層次越低越好。舉個例子,Thrift接口調(diào)用時,數(shù)據(jù)傳輸失敗需要引入自動重試機制,重試的策略顯然在Thrift內(nèi)部封裝更合適,開放給用戶( 下游開發(fā)人員 )會增加額外的使用負擔。與之類似的是系統(tǒng)里隨處可見的配置參數(shù)( 通常寫在XML文件里 ),在編程中應當盡量避免這種情況,用戶( 下游開發(fā)人員 )一般很難決定哪個參數(shù)是最優(yōu)的,如果一定要開放參數(shù)配置,最好給定一個默認值。

復雜性下沉,并不是說把所有功能下移到一個層次,過猶不及。如果復雜性跟下層的功能相關,或者下移后,能大大下降其他層次或整體的復雜性,則下移。

5.3 異常處理

異常和錯誤處理是造成軟件復雜的罪魁禍首之一。有些開發(fā)人員錯誤的認為處理和上報的錯誤越多越好,這會導致過度防御性的編程。如果開發(fā)人員捕獲了異常并不知道如何處理,直接往上層扔,這就違背了封裝原則。

降低復雜度的一個原則就是盡可能減少需要處理異常的可能性。而最佳實踐就是確保錯誤終結,例如刪除一個并不存在的文件,與其上報文件不存在的異常,不如什么都不做。確保文件不存在就好了,上層邏輯不但不會被影響,還會因為不需要處理額外的異常而變得簡單。

六、解決復雜性之分模塊

分模塊是解決復雜性的重要方法。理想情況下,模塊之間應該是相互隔離的,開發(fā)人員面對具體的任務,只需要接觸和了解整個系統(tǒng)的一小部分,而無需了解或改動其他模塊。

6.1 深模塊和淺模塊

深模塊( Deep Module )指的是擁有強大功能和簡單接口的模塊。深模塊是抽象的最佳實踐,通過排除模塊內(nèi)部不重要的信息,讓用戶更容易理解和使用。

Unix操作系統(tǒng)文件I/O是典型的深模塊,以Open函數(shù)為例,接口接受文件名為參數(shù),返回文件描述符。但是這個接口的背后,是幾百行的實現(xiàn)代碼,用來處理文件存儲、權限控制、并發(fā)控制、存儲介質(zhì)等等,這些對用戶是不可見的。

  1. int open(const char* path, int flags, mode_t permissions); 

與深模塊相對的是淺模塊( Shallow Module ),功能簡單,接口復雜。通常情況下,淺模塊無助于解決復雜性。因為他們提供的收益( 功能 )被學習和使用成本抵消了。以Java I/O為例,從I/O中讀取對象時,需要同時創(chuàng)建三個對象FileInputStream、BufferedInputStream、ObjectInputStream,其中前兩個創(chuàng)建后不會被直接使用,這就給開發(fā)人員造成了額外的負擔。默認情況下,開發(fā)人員無需感知到BufferedInputStream,緩沖功能有助于改善文件I/O性能,是個很有用的特性,可以合并到文件I/O對象里。假如我們想放棄緩沖功能,文件I/O也可以設計成提供對應的定制選項。

  1. FileInputStream fileStream = new FileInputStream(fileName); 
  2. BufferedInputStream bufferedStream = new BufferedInputStream(fileStream); 
  3. ObjectInputStream objectStream = new ObjectInputStream(bufferedStream); 

關于淺模塊有一些爭議,大多數(shù)情況是因為淺模塊是不得不接受的既定事實,而不見得是因為合理性。當然也有例外,比如領域驅(qū)動設計里的防腐層,系統(tǒng)在與外部系統(tǒng)對接時,會單獨建立一個服務或模塊去適配,用來保證原有系統(tǒng)技術棧的統(tǒng)一和穩(wěn)定性。

6.2 通用和專用

設計新模塊時,應該設計成通用模塊還是專用模塊?一種觀點認為通用模塊滿足多種場景,在未來遇到預期外的需求時,可以節(jié)省時間。另外一種觀點則認為,未來的需求很難預測,沒必要引入用不到的特性,專用模塊可以快速滿足當前的需求,等有后續(xù)需求時再重構成通用的模塊也不遲。

以上兩種思路都有道理,實際操作的時候可以采用兩種方式各自的優(yōu)點,即在功能實現(xiàn)上滿足當前的需求,便于快速實現(xiàn);接口設計通用化,為未來留下余量。舉個例子。

  1. void backspace(Cursor cursor); 
  2. void delete(Cursor cursor); 
  3. void deleteSelection(Selection selection); 
  4.  
  5. //以上三個函數(shù)可以合并為一個更通用的函數(shù) 
  6. void delete(Position start, Position end); 

設計通用性接口需要權衡,既要滿足當前的需求,同時在通用性方面不要過度設計。一些可供參考的標準:

  • 滿足當前需求最簡單的接口是什么?在不減少功能的前提下,減少方法的數(shù)量,意味著接口的通用性提升了。

  • 接口使用的場景有多少?如果接口只有一個特定的場景,可以將多個這樣的接口合并成通用接口。

  • 滿足當前需求情況下,接口的易用性如何?如果接口很難使用,意味著我們可能過度設計了,需要拆分。

6.3 信息隱藏

信息隱藏是指,程序的設計思路以及內(nèi)部邏輯應當包含在模塊內(nèi)部,對其他模塊不可見。如果一個模塊隱藏了很多信息,說明這個模塊在提供很多功能的同時又簡化了接口,符合前面提到的深模塊理念。軟件設計領域有個技巧,定義一個“大”類有助于實現(xiàn)信息隱藏。這里的“大”類指的是,如果要實現(xiàn)某功能,將該功能相關的信息都封裝進一個類里面。

信息隱藏在降低復雜性方面主要有兩個作用:一是簡化模塊接口,將模塊功能以更簡單、更抽象的方式表現(xiàn)出來,降低開發(fā)人員的認知負擔;二是減少模塊間的依賴,使得系統(tǒng)迭代更輕量。舉個例子,如何從B+樹中存取信息是一些數(shù)據(jù)庫索引的核心功能,但是數(shù)據(jù)庫開發(fā)人員將這些信息隱藏了起來,同時提供簡單的對外交互接口,也就是SQL腳本,使得產(chǎn)品和運營同學也能很快地上手。并且,因為有足夠的抽象,數(shù)據(jù)庫可以在保持外部兼容的情況下,將索引切換到散列或其他數(shù)據(jù)結構。

與信息隱藏相對的是信息暴露,表現(xiàn)為:設計決策體現(xiàn)在多個模塊,造成不同模塊間的依賴。舉個例子,兩個類能處理同類型的文件。這種情況下,可以合并這兩個類,或者提煉出一個新類( 參考《重構》[3]一書 )。工程師應當盡量減少外部模塊需要的信息量。

6.4 拆分和合并

兩個功能,應該放在一起還是分開?“不管黑貓白貓”,能降低復雜性就好。這里有一些可以借鑒的設計思路:

  • 共享信息的模塊應當合并,比如兩個模塊都依賴某個配置項。

  • 可以簡化接口時合并,這樣可以避免客戶同時調(diào)用多個模塊來完成某個功能。

  • 可以消除重復時合并,比如抽離重復的代碼到一個單獨的方法中。

  • 通用代碼和專用代碼分離,如果模塊的部分功能可以通用,建議和專用部分分離。舉個例子,在實際的系統(tǒng)設計中,我們會將專用模塊放在上層,通用模塊放在下層以供復用。

七、解決復雜性之注釋

注釋可以記錄開發(fā)人員的設計思路和程序功能,降低開發(fā)人員的認知負擔和解決不可知( Unkown Unkowns )問題,讓代碼更容易維護。通常情況下,在程序的整個生命周期里,編碼只占了少部分,大量時間花在了后續(xù)的維護上。有經(jīng)驗的工程師懂得這個道理,通常也會產(chǎn)出更高質(zhì)量的注釋和文檔。

注釋也可以作為系統(tǒng)設計的工具,如果只需要簡單的注釋就可以描述模塊的設計思路和功能,說明這個模塊的設計是良好的。另一方面,如果模塊很難注釋,說明模塊沒有好的抽象。

7.1 注釋的誤區(qū)

關于注釋,很多開發(fā)者存在一些認識上的誤區(qū),也是造成大家不愿意寫注釋的原因。比如“好代碼是自注釋的”、“沒有時間”、“現(xiàn)有的注釋都沒有用,為什么還要浪費時間”等等。這些觀點是站不住腳的。“好代碼是自注釋的”只在某些場景下是合理的,比如為變量和方法選擇合適的名稱,可以不用單獨注釋。但是更多的情況,代碼很難體現(xiàn)開發(fā)人員的設計思路。此外,如果用戶只能通過讀代碼來理解模塊的使用,說明代碼里沒有抽象。好的注釋可以極大地提升系統(tǒng)的可維護性,獲取長期的效率,不存在“沒有時間”一說。注釋也是一種可以習得的技能,一旦習得,就可以在后續(xù)的工作中應用,這就解決了“注釋沒有用”的問題。

7.2 使用注釋提升系統(tǒng)可維護性

注釋應當能提供代碼之外額外的信息,重視What和Why,而不是代碼是如何實現(xiàn)的( How),最好不要簡單地使用代碼中出現(xiàn)過的單詞。

根據(jù)抽象程度,注釋可以分為低層注釋和高層注釋,低層次的注釋用來增加精確度,補充完善程序的信息,比如變量的單位、控制條件的邊界、值是否允許為空、是否需要釋放資源等。高層次注釋拋棄細節(jié),只從整體上幫助讀者理解代碼的功能和結構。這種類型的注釋更好維護,如果代碼修改不影響整體的功能,注釋就無需更新。在實際工作中,需要兼顧細節(jié)和抽象。低層注釋拆散與對應的實現(xiàn)代碼放在一起,高層注釋一般用于描述接口。

注釋先行,注釋應該作為設計過程的一部分,寫注釋最好的時機是在開發(fā)的開始環(huán)節(jié),這不僅會產(chǎn)生更好的文檔,也會幫助產(chǎn)生好的設計,同時減少寫文檔帶來的痛苦。開發(fā)人員推遲寫注釋的理由通常是:代碼還在修改中,提前寫注釋到時候還得再改一遍。這樣的話就會衍生兩個問題:

  • 首先,推遲注釋通常意味著根本就沒有注釋。一旦決定推遲,很容易引發(fā)連鎖反應,等到代碼穩(wěn)定后,也不會有注釋這回事。這時候再想添加注釋,就得專門抽出時間,客觀條件可能不會允許這么做。

  • 其次,就算我們足夠自律抽出專門時間去寫注釋,注釋的質(zhì)量也不會很好。我們潛意識中覺得代碼已經(jīng)寫完了,急于開展下一個項目,只是象征性地添加一些注釋,無法準確復現(xiàn)當時的設計思路。

避免重復的注釋。如果有重復注釋,開發(fā)人員很難找到所有的注釋去更新。解決方法是,可以找到醒目的地方存放注釋文檔,然后在代碼處注明去查閱對應文檔的地址。如果程序已經(jīng)在外部文檔中注釋過了,不要在程序內(nèi)部再注釋了,添加注釋的引用就可以了。

注釋屬于代碼,而不是提交記錄。一種錯誤的做法是將功能注釋放在提交記錄里,而不是放在對應代碼文件里。因為開發(fā)人員通常不會去代碼提交記錄里去查看程序的功能描述,很不方便。

7.3 使用注釋改善系統(tǒng)設計

良好的設計基礎是提供好的抽象。在開始編碼前編寫注釋,可以幫助我們提煉模塊的核心要素:模塊或?qū)ο笾凶钪匾墓δ芎蛯傩浴_@個過程促進我們?nèi)ニ伎?,而不是簡單地堆砌代碼。另一方面,注釋也能夠幫助我們檢查自己的模塊設計是否合理,正如前文中提到,深模塊提供簡單的接口和強大的功能,如果接口注釋冗長復雜,通常意味著接口也很復雜;注釋簡單,意味著接口也很簡單。在設計的早期注意和解決這些問題,會為我們帶來長期的收益。

八、后記

有經(jīng)驗的工程師看到這些觀點會有共鳴,一些著作如《代碼大全》、《領域驅(qū)動設計》也會有類似的觀點。 所以本文中提到的原則和方法具有一定實操和指導價值。 對于很難有定論的問題,也可以在實踐中去探索。

關于原則和方法論,既不必刻意拔高,也不要嗤之以鼻。 指導實踐的不是更多的實踐,而是實踐后的總結和思考。 應用原則和方法論實質(zhì)是借鑒已有的經(jīng)驗,可以減少我們自行摸索的時間 。探索新的方法可以幫助我們適應新的場景,但是新方法本身需要經(jīng)過時間檢驗。

九、參考文檔

[1] John Ousterhout.  A philosophy of software design . Yaknyam Press, 2018.

[2] 梅拉尼·米歇爾. 復雜. 湖南科學技術出版社, 2016.

[3] Martin Fowler.   Refactoring: Improving the Design of Existing Code ( 2nd Edition )   . Addison-Wesley Signature Series, 2018.

作者介紹

政華,順譜,陶鑫,美團打車調(diào)度系統(tǒng)工程團隊工程師。

 

責任編輯:張燕妮 來源: 美團技術團隊
相關推薦

2020-12-30 09:20:27

代碼

2024-07-30 10:55:25

2024-04-16 08:19:40

架構高可用消息隊列

2022-08-16 09:04:23

代碼圈圈復雜度節(jié)點

2023-03-03 08:43:08

代碼重構系統(tǒng)

2020-06-01 08:42:11

JavaScript重構函數(shù)

2024-06-05 09:35:00

2013-08-01 13:18:41

代碼

2011-06-07 10:30:54

2023-10-05 11:08:53

2019-10-14 17:00:14

前端代碼圈復雜度

2022-02-23 11:49:25

自動化云基礎設施

2019-12-24 09:46:00

Linux設置密碼

2022-12-25 18:58:53

架構RabbitMQ

2022-05-28 16:08:04

前端

2015-07-30 11:21:16

代碼審查

2018-08-14 14:08:55

虛擬化數(shù)據(jù)中心存儲

2016-05-24 10:43:25

非科班碼農(nóng)

2024-04-25 08:33:25

算法時間復雜度空間復雜度

2021-01-05 10:41:42

算法時間空間
點贊
收藏

51CTO技術棧公眾號