如何降低軟件復雜性?讓代碼更加精簡

      網友投稿 820 2022-05-29

      前言

      在進行軟件開發時,我們常常會追求軟件的高可維護性,高可維護性意味著當有新需求來時,系統易擴展;當出現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提出了一個公式來度量一個系統的復雜性:

      式中,表示系統中的模塊,表示該模塊的認知負擔(Cognitive Load,即一個模塊難以理解的程度),表示在日常開發中在該模塊花費的開發時間。

      從公式上看,一個軟件的復雜性由它的各個模塊的復雜性累加而成,而?模塊復雜性 = 模塊認知負擔 * 模塊開發時間,也就是模塊的復雜性即和模塊本身有關,也跟在該模塊上花費的開發時間有關。需要注意的是,如果一個模塊非常難以理解,但是后續開發過程中幾乎沒有涉及到它,那么它的復雜性也是很低的。

      導致軟件復雜的原因

      導致軟件復雜的原因可以細分出很多種來,而概括起來莫過于兩種:依賴(dependencies)?和?隱晦(obscurity)。前者會讓修改起來很費勁而且容易出現bug,比如當修改模塊1時,往往也涉及到模塊2、模塊3、...?的改動;后者會讓軟件難以理解,定位一個bug,甚至是僅僅讀懂一段代碼都需要花費大量的時間。

      軟件的復雜性往往伴隨著如下幾種癥狀:

      霰彈式修改(Change amplification)。當只需要修改一個功能,但又不得不對許多模塊作出改動時,我們稱之為霰彈式修改。這通常是因為模塊之間耦合過重,相互依賴太多導致的。 比如,有一組Web頁面,每個頁面都是一個HTML文件,每個HTML都有一個背景屬性。由于各個HTML的背景屬性都是分開定義的,因此如果需要把背景顏色從橙色修改為藍色時,就需要改動所有的HTML文件。

      霰彈式修改的典型例子

      認知負擔(Cognitive load)。當我們說一個模塊隱晦、難以理解時,它就有過重的認知負擔,這種情況下往往需要讀者花費大量時間才能明白該模塊的功能。比如,提供一個不帶任何注釋的calculate接口,它有2個int類型的入參和一個int類型的返回值。從該函數的簽名上看,調用者根本無法得知函數的功能是什么,他只能通過花時間去閱讀源碼來確定函數功能后才敢去調用該函數。

      int?calculate(int?val1,?int?val2);

      不確定性(Unknown unknowns)。相比于前兩種癥狀,不確定性的破壞性更大,它通常指一些在開發需求時,你必須注意的,但是又無從得知的點。它常常是因為一些隱晦的依賴導致的,會讓你在開發完一個需求之后感覺心里很沒譜,隱約覺得自己的代碼哪里有問題,但又不清楚問題在哪,只能祈禱在測試階段能夠暴露而不要漏洞商用階段。

      如何降低軟件的復雜性

      對 “戰術編程” Say No!

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

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

      戰術編程 VS 戰略編程

      讓模塊更“深”一點!

      一個模塊由接口(interface)和實現(implementation)兩部分組成,如果把一個模塊比喻成一個矩形,那么接口就是矩形頂部的邊,而實現就是矩形的面積(也可以把實現看成是模塊提供的功能)。當一個模塊提供的功能一定時,深模塊(Deep module)的特點就是矩形頂部的邊比較短,整體形狀高瘦,也即接口比較簡單;淺模塊(Shallow module)的特點就是矩形頂部的邊比較長,整體形狀矮胖,也即接口比較復雜。

      深模塊 VS 淺模塊

      模塊的使用者往往只看到接口,模塊越深,模塊暴露給調用者的信息就越少,調用者與該模塊的耦合性也就越低。因此,把模塊設計得更“深”一點,有助于降低系統的復雜性。

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

      更簡單的接口

      簡單的接口比簡單的實現更重要,更簡單的接口意味著模塊的易用性更好,調用者使用起來更方便。而簡單的實現 + 復雜的接口這種形式,一方面影響了接口的易用性,另一方面則加深了調用者與模塊的耦合。因此,在進行模塊設計時,最好遵守“把簡單留給別人,把復雜留給自己”的原則。

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

      簡單的接口比簡單的實現更重要,更簡單的接口意味著模塊的易用性更好,調用者使用起來更方便。而簡單的實現 + 復雜的接口這種形式,一方面影響了接口的易用性,另一方面則加深了調用者與模塊的耦合。因此,在進行模塊設計時,最好遵守“把簡單留給別人,把復雜留給自己”的原則。

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

      如何降低軟件復雜性?讓代碼更加精簡

      更通用的接口

      在設計接口時,你往往有兩種選擇:(1)設計成專用的接口;(2)設計成通用的接口。前者實現起來更方便,而且完全可以滿足當前的需求,但可擴展性低,屬于戰術編程;后者則需要花時間對系統進行抽象,但可擴展性高,屬于戰略編程。通用的接口意味著該接口適用的場景不止一個,典型的就是“?一個接口,多個實現?”的形式。

      有些程序員可能會反駁,在無法預知未來變化的情況下,通用就意味著過度設計。過度通用確實屬于過度設計,但對接口進行適度的抽象并不是,相反它可以使系統更有層次感,可維護性也更高。

      在設計接口時,你往往有兩種選擇:(1)設計成專用的接口;(2)設計成通用的接口。前者實現起來更方便,而且完全可以滿足當前的需求,但可擴展性低,屬于戰術編程;后者則需要花時間對系統進行抽象,但可擴展性高,屬于戰略編程。通用的接口意味著該接口適用的場景不止一個,典型的就是“?一個接口,多個實現?”的形式。

      有些程序員可能會反駁,在無法預知未來變化的情況下,通用就意味著過度設計。過度通用確實屬于過度設計,但對接口進行適度的抽象并不是,相反它可以使系統更有層次感,可維護性也更高。

      隱藏細節

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

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

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

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

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

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

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

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

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

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

      進行分層設計!

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

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

      所謂的Pass-Through Mehod是指那些“在函數體內直接調用其他函數,而本身只做了極少的事情”的函數,通常其函數簽名與被其調用的函數簽名很類似。Pass-Through Mehod所在的模塊通常都是淺模塊,讓系統增加了無謂的層次和函數調用,會使系統更加復雜。

      Pass-Through Mehod(選自《A Philosophy of Software Design》中的例子)

      學會寫代碼注釋!

      注釋是降低軟件復雜性的性價比極高的一種手法,它只需要花費20%的時間,即可獲取80%的價值。它可以提高晦澀難懂的代碼的可讀性;可以起到隱藏代碼復雜細節的作用,比如接口注釋可以幫助開發者在沒有閱讀代碼的情況下快速了解該接口的功能和用法;如果寫的好,它還可以改善系統的設計。

      具體如何寫好代碼注釋,參考《教你寫好代碼注釋》一文。

      總結

      軟件的復雜性是我們程序員在日常開發中所必須面對的東西,學會如何 “弄清楚什么是軟件復雜性,找到導致軟件復雜的原因,并利用各種手法去戰勝軟件的復雜性” 是一門必備的能力。有句話說得很好,“代碼質量決定生活質量”,當你把軟件的復雜性降低了,bug減少了,系統可維護性更高了,自然也就帶來了更好的生活質量。

      模塊設計是降低軟件復雜度最有效的手段,學會使用“戰略編程”的方法,并堅持下去。我們常常提倡“一次把事情做對”,但這對于模塊設計而言并不適用,幾乎沒有人可以第一次就把一個模塊設計成完美的模樣。二次設計是一個非常有效的手法,與其在系統腐化之后再花大量時間進行重構或重寫,還不如在第一次完成模塊設計后,再花點時間進行二次設計,多問問自己:是否有更簡單的接口?是否有更通用的設計?是否有更簡潔高效的實現?

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

      軟件開發

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:Javaweb之實現文件上傳+點擊下載功能
      下一篇:【JavaSE】繼承基本使用
      相關文章
      亚洲偷自精品三十六区| 亚洲精选在线观看| 久久亚洲最大成人网4438| 久久丫精品国产亚洲av| 亚洲αv在线精品糸列| 亚洲日韩欧洲无码av夜夜摸| 国产亚洲精品资在线| 久久精品国产亚洲Aⅴ蜜臀色欲 | 亚洲日本视频在线观看| 亚洲国产精品国自产拍电影| 亚洲一区二区三区日本久久九| 亚洲Aⅴ无码专区在线观看q| 久久久亚洲精品国产| 亚洲AV无码国产在丝袜线观看| 国产亚洲精品资源在线26u| 精品亚洲永久免费精品| 亚洲国产精品一区第二页| 国产亚洲综合成人91精品| 亚洲AV永久精品爱情岛论坛| 亚洲第一福利网站| 亚洲综合综合在线| 亚洲一级片在线观看| 中文字幕亚洲情99在线| 亚洲国产成人久久精品大牛影视| 亚洲AV日韩AV永久无码色欲| 无码不卡亚洲成?人片| 亚洲精品国产自在久久| 黑人大战亚洲人精品一区| 国产亚洲精品岁国产微拍精品| 亚洲产国偷V产偷V自拍色戒| 亚洲伦理一区二区| 亚洲一区二区久久| 亚洲成AV人影片在线观看| 无码专区一va亚洲v专区在线| 亚洲综合亚洲综合网成人| 亚洲精品国产美女久久久| 亚洲天天做日日做天天欢毛片| 亚洲国产精品久久久久秋霞影院 | 亚洲综合另类小说色区色噜噜| 精品亚洲永久免费精品| 亚洲熟妇色自偷自拍另类|