爲啥你寫的代碼老是這麼複雜?

摘要:有句話說得很好,「代碼質量決定生活質量」,當你把軟件的複雜性下降了,bug減小了,系統可維護性更高了,天然也就帶來了更好的生活質量。

本文分享自華爲雲社區《寫出的代碼複雜度過高?看下專家怎麼說》,原文做者:元閏子 。程序員

前言

在進行軟件開發時,咱們經常會追求軟件的高可維護性,高可維護性意味着當有新需求來時,系統易擴展;當出現bug時,開發人員易定位。而當咱們說一個系統的可維護性太差時,每每指的是該系統太過複雜,致使給系統增長新功能時容易出現bug,而出現bug以後又難以定位。數據庫

那麼,軟件的複雜性又是如何定義的呢?編程

John Ousterhout給出的定義以下:安全

Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.

可見,軟件的複雜性是一個很泛的概念,任何使軟件難以理解和難以修改的東西,都屬於軟件的複雜性。爲此,John Ousterhout提出了一個公式來度量一個系統的複雜性:多線程

式中,pp表示系統中的模塊,c_{p}cp​表示該模塊的認知負擔(Cognitive Load,即一個模塊難以理解的程度),t_{p}tp​表示在平常開發中在該模塊花費的開發時間。架構

從公式上看,一個軟件的複雜性由它的各個模塊的複雜性累加而成,而 模塊複雜性 = 模塊認知負擔 * 模塊開發時間,也就是模塊的複雜性即和模塊自己有關,也跟在該模塊上花費的開發時間有關。須要注意的是,若是一個模塊很是難以理解,可是後續開發過程當中幾乎沒有涉及到它,那麼它的複雜性也是很低的。函數

致使軟件複雜的緣由

致使軟件複雜的緣由能夠細分出不少種來,而歸納起來莫過於兩種:依賴(dependencies)  隱晦(obscurity)。前者會讓修改起來很費勁並且容易出現bug,好比當修改模塊1時,每每也涉及到模塊二、模塊三、... 的改動;後者會讓軟件難以理解,定位一個bug,甚至是僅僅讀懂一段代碼都須要花費大量的時間。測試

軟件的複雜性每每伴隨着以下幾種症狀:編碼

霰彈式修改(Change amplification)。當只須要修改一個功能,但又不得不對許多模塊做出改動時,咱們稱之爲霰彈式修改。這一般是由於模塊之間耦合太重,相互依賴太多致使的。 好比,有一組Web頁面,每一個頁面都是一個HTML文件,每一個HTML都有一個背景屬性。因爲各個HTML的背景屬性都是分開定義的,所以若是須要把背景顏色從橙色修改成藍色時,就須要改動全部的HTML文件。url

認知負擔(Cognitive load)。當咱們說一個模塊隱晦、難以理解時,它就有太重的認知負擔,這種狀況下每每須要讀者花費大量時間才能明白該模塊的功能。好比,提供一個不帶任何註釋的calculate接口,它有2個int類型的入參和一個int類型的返回值。從該函數的簽名上看,調用者根本沒法得知函數的功能是什麼,他只能經過花時間去閱讀源碼來肯定函數功能後纔敢去調用該函數。

int calculate(int val1, int val2);

不肯定性(Unknown unknowns)。相比於前兩種症狀,不肯定性的破壞性更大,它一般指一些在開發需求時,你必須注意的,可是又無從得知的點。它經常是由於一些隱晦的依賴致使的,會讓你在開發完一個需求以後感受內心很沒譜,隱約以爲本身的代碼哪裏有問題,但又不清楚問題在哪,只能祈禱在測試階段可以暴露而不要漏洞商用階段。

如何下降軟件的複雜性

對 「戰術編程」 Say No!

不少程序員在進行特性開發或bug修復時,關注點每每是如何簡單快速讓程序跑起來,這就是典型的戰術編程(Tactical programming)方法,它追求的是短時間的效益——節省開發時間。戰術編程最廣泛的體現就是在編碼以前沒有進行模塊設計,想到哪裏就寫到哪裏。戰術編程在系統前期可能會比較方便,一旦系統龐大起來、模塊之間的耦合變重以後,添加或修改功能、修復bug都會變得步履維艱。隨着系統變得愈來愈複雜,最後不得不對系統進行重構甚至重寫。

與戰術編程相對的就是戰略編程(Strategic programming),它追求的是長期的效益——增長系統可維護性。僅僅是讓程序跑起來還不足以知足,還須要考慮程序的可維護性,讓後續在添加或修改功能、修復bug時都可以快速響應。由於考慮的點比較多,也就註定戰略編程須要花費必定的時間去進行模塊設計,但相比於戰術編程後期致使的問題,這一點時間也是徹底值得的。

讓模塊更「深」一點!

一個模塊由接口(interface)和實現(implementation)兩部分組成,若是把一個模塊比喻成一個矩形,那麼接口就是矩形頂部的邊,而實現就是矩形的面積(也能夠把實現當作是模塊提供的功能)。當一個模塊提供的功能必定時,深模塊(Deep module)的特色就是矩形頂部的邊比較短,總體形狀高瘦,也即接口比較簡單;淺模塊(Shallow module)的特色就是矩形頂部的邊比較長,總體形狀矮胖,也即接口比較複雜。

模塊的使用者每每只看到接口,模塊越深,模塊暴露給調用者的信息就越少,調用者與該模塊的耦合性也就越低。所以,把模塊設計得更「深」一點,有助於下降系統的複雜性。

那麼,怎樣才能設計出一個深模塊呢?

  • 更簡單的接口

簡單的接口比簡單的實現更重要,更簡單的接口意味着模塊的易用性更好,調用者使用起來更方便。而簡單的實現 + 複雜的接口這種形式,一方面影響了接口的易用性,另外一方面則加深了調用者與模塊的耦合。所以,在進行模塊設計時,最好遵照「把簡單留給別人,把複雜留給本身」的原則。

異常也屬於接口的一部分,在編碼過程當中,應該杜絕沒通過處理,就隨意將異常往上拋的現象,這樣只會增長系統的複雜性。

  • 更通用的接口

在設計接口時,你每每有兩種選擇:(1)設計成專用的接口;(2)設計成通用的接口。前者實現起來更方便,並且徹底能夠知足當前的需求,但可擴展性低,屬於戰術編程;後者則須要花時間對系統進行抽象,但可擴展性高,屬於戰略編程。通用的接口意味着該接口適用的場景不止一個,典型的就是「 一個接口,多個實現 」的形式。

有些程序員可能會反駁,在沒法預知將來變化的狀況下,通用就意味着過分設計。過分通用確實屬於過分設計,但對接口進行適度的抽象並非,相反它可使系統更有層次感,可維護性也更高。

  • 隱藏細節

在進行模塊設計時,還要學會區分對於調用者而言,哪些信息是重要的,哪些信息是不重要的。隱藏細節指的就是只給調用者暴露重要的信息,把不重要的細節隱藏起來。隱藏細節一則使模塊接口更簡單,二則使系統更易維護。

如何判斷細節對於調用者是否重要?如下有幾個例子:

一、對於Java的Map接口,重要的細節:Map中每個元素都是由<Key, Value>組成的;不重要的細節:Map底層是如何存儲這些元素、如何實現線程安全等。

二、對於文件系統中的read函數,重要的細節:每次讀操做從哪一個文件讀、讀多少字節;不重要的細節:如何切換到內核態、如何從硬盤裏讀數據等。

三、對於多線程應用程序,重要的細節:如何建立一個線程;不重要的細節:多核CPU如何調度該線程。

進行分層設計!

設計良好的軟件架構都有一個特色,就是層次清晰,每一層都提供了不一樣的抽象,各個層次之間的依賴明確。無論是經典的Web三層架構、DDD所提倡的四層架構以及六邊形架構,抑或是所謂的Clean Architecture,都有着鮮明的層次感。

在進行分層設計時,須要注意的是,每一層都應該提供不一樣的抽象,並要儘可能避免在一個模塊中出現大量的Pass-Through Mehod。好比在DDD的四層架構中,領域層提供了對領域業務邏輯的抽象,應用層提供了對系統用例的抽象,接口層提供了對系統訪問接口的抽象,基礎設施層則提供對如數據庫訪問這類的基礎服務的抽象。

所謂的Pass-Through Mehod是指那些「在函數體內直接調用其餘函數,而自己只作了極少的事情」的函數,一般其函數簽名與被其調用的函數簽名很相似。Pass-Through Mehod所在的模塊一般都是淺模塊,讓系統增長了無謂的層次和函數調用,會使系統更加複雜:

public class TextDocument ... {
  private TextArea textArea;
  private TextDocumentListener listener;
  ...
  public Character getLastTypedCharacter() {
    return textArea.getLastTypedCharacter();
  }
  public int getCursorOffset() {
    return textArea.getCursorOffset();
  }
  public void insertString(String textToInsert, int offset) {
    textArea.insertString(textToInsert, offset);
  }
  ...
}

學會寫代碼註釋!

註釋是軟件開發過程當中的性價比極高的一種手法,它只須要花費20%的時間,便可獲取80%的價值。它能夠提升晦澀難懂的代碼的可讀性;能夠起到隱藏代碼複雜細節的做用,好比接口註釋能夠幫助開發者在沒有閱讀代碼的狀況下快速瞭解該接口的功能和用法;若是寫的好,它還能夠改善系統的設計

具體如何寫好代碼註釋,參考《如何寫出優秀的代碼註釋?》一文。

總結

軟件的複雜性是咱們程序員在平常開發中所必須面對的東西,學會如何 「弄清楚什麼是軟件複雜性,找到致使軟件複雜的緣由,並利用各類手法去打敗軟件的複雜性」 是一門必備的能力。有句話說得很好,「代碼質量決定生活質量」,當你把軟件的複雜性下降了,bug減小了,系統可維護性更高了,天然也就帶來了更好的生活質量。

模塊設計是下降軟件複雜度最有效的手段,學會使用「戰略編程」的方法,並堅持下去。咱們經常提倡「一次把事情作對」,但這對於模塊設計而言並不適用,幾乎沒有人能夠第一次就把一個模塊設計成完美的模樣。二次設計是一個很是有效的手法,與其在系統腐化以後再花大量時間進行重構或重寫,還不如在第一次完成模塊設計後,再花點時間進行二次設計,多問問本身:是否有更簡單的接口?是否有更通用的設計?是否有更簡潔高效的實現?

"羅馬不是一天建成的",下降軟件的複雜性也同樣,貴在堅持。

 

點擊關注,第一時間瞭解華爲雲新鮮技術~

相關文章
相關標籤/搜索