一步步降低軟件復(fù)雜性

前言

在進(jìn)行軟件開發(fā)時喇肋,我們常常會追求軟件的高可維護(hù)性,高可維護(hù)性意味著當(dāng)有新需求來時蜕青,系統(tǒng)易擴(kuò)展苟蹈;當(dāng)出現(xiàn)bug時,開發(fā)人員易定位右核。而當(dāng)我們說一個系統(tǒng)的可維護(hù)性太差時慧脱,往往指的是該系統(tǒng)太過復(fù)雜,導(dǎo)致給系統(tǒng)增加新功能時容易出現(xiàn)bug贺喝,而出現(xiàn)bug之后又難以定位菱鸥。

那么,軟件的復(fù)雜性又是如何定義的呢躏鱼?

John Ousterhout給出的定義如下:

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

可見氮采,軟件的復(fù)雜性是一個很泛的概念,任何使軟件難以理解和難以修改的東西染苛,都屬于軟件的復(fù)雜性鹊漠。為此主到,John Ousterhout提出了一個公式來度量一個系統(tǒng)的復(fù)雜性:
C = \sum_{p}c_{p}t_{p}

式中,p表示系統(tǒng)中的模塊躯概,c_{p}表示該模塊的認(rèn)知負(fù)擔(dān)(Cognitive Load登钥,即一個模塊難以理解的程度),t_{p}表示在日常開發(fā)中在該模塊花費的開發(fā)時間娶靡。

從公式上看牧牢,一個軟件的復(fù)雜性由它的各個模塊的復(fù)雜性累加而成,而 模塊復(fù)雜性 = 模塊認(rèn)知負(fù)擔(dān) * 模塊開發(fā)時間姿锭,也就是模塊的復(fù)雜性即和模塊本身有關(guān)塔鳍,也跟在該模塊上花費的開發(fā)時間有關(guān)。需要注意的是呻此,如果一個模塊非常難以理解轮纫,但是后續(xù)開發(fā)過程中幾乎沒有涉及到它,那么它的復(fù)雜性也是很低的趾诗。

導(dǎo)致軟件復(fù)雜的原因

導(dǎo)致軟件復(fù)雜的原因可以細(xì)分出很多種來蜡感,而概括起來莫過于兩種:依賴(dependencies)隱晦(obscurity)蹬蚁。前者會讓修改起來很費勁而且容易出現(xiàn)bug恃泪,比如當(dāng)修改模塊1時,往往也涉及到模塊2犀斋、模塊3贝乎、... 的改動;后者會讓軟件難以理解叽粹,定位一個bug览效,甚至是僅僅讀懂一段代碼都需要花費大量的時間。

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

霰彈式修改(Change amplification)虫几。當(dāng)只需要修改一個功能锤灿,但又不得不對許多模塊作出改動時,我們稱之為霰彈式修改辆脸。這通常是因為模塊之間耦合過重但校,相互依賴太多導(dǎo)致的。 比如啡氢,有一組Web頁面状囱,每個頁面都是一個HTML文件,每個HTML都有一個背景屬性倘是。由于各個HTML的背景屬性都是分開定義的亭枷,因此如果需要把背景顏色從橙色修改為藍(lán)色時,就需要改動所有的HTML文件搀崭。

霰彈式修改的典型例子

認(rèn)知負(fù)擔(dān)(Cognitive load)叨粘。當(dāng)我們說一個模塊隱晦、難以理解時,它就有過重的認(rèn)知負(fù)擔(dān)升敲,這種情況下往往需要讀者花費大量時間才能明白該模塊的功能袍镀。比如,提供一個不帶任何注釋的calculate接口冻晤,它有2個int類型的入?yún)⒑鸵粋€int類型的返回值苇羡。從該函數(shù)的簽名上看,調(diào)用者根本無法得知函數(shù)的功能是什么鼻弧,他只能通過花時間去閱讀源碼來確定函數(shù)功能后才敢去調(diào)用該函數(shù)设江。

int calculate(int val1, int val2);

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

如何降低軟件的復(fù)雜性

對 “戰(zhàn)術(shù)編程” Say No练俐!

很多程序員在進(jìn)行特性開發(fā)或bug修復(fù)時,關(guān)注點往往是如何簡單快速讓程序跑起來冕臭,這就是典型的戰(zhàn)術(shù)編程(Tactical programming)方法腺晾,它追求的是短期的效益——節(jié)省開發(fā)時間。戰(zhàn)術(shù)編程最普遍的體現(xiàn)就是在編碼之前沒有進(jìn)行模塊設(shè)計辜贵,想到哪里就寫到哪里悯蝉。戰(zhàn)術(shù)編程在系統(tǒng)前期可能會比較方便,一旦系統(tǒng)龐大起來托慨、模塊之間的耦合變重之后鼻由,添加或修改功能、修復(fù)bug都會變得寸步難行榴芳。隨著系統(tǒng)變得越來越復(fù)雜嗡靡,最后不得不對系統(tǒng)進(jìn)行重構(gòu)甚至重寫。

與戰(zhàn)術(shù)編程相對的就是戰(zhàn)略編程(Strategic programming)窟感,它追求的是長期的效益——增加系統(tǒng)可維護(hù)性讨彼。僅僅是讓程序跑起來還不足以滿足,還需要考慮程序的可維護(hù)性柿祈,讓后續(xù)在添加或修改功能哈误、修復(fù)bug時都能夠快速響應(yīng)哩至。因為考慮的點比較多,也就注定戰(zhàn)略編程需要花費一定的時間去進(jìn)行模塊設(shè)計蜜自,但相比于戰(zhàn)術(shù)編程后期導(dǎo)致的問題菩貌,這一點時間也是完全值得的。

戰(zhàn)術(shù)編程 VS 戰(zhàn)略編程

讓模塊更“深”一點重荠!

一個模塊由接口(interface)和實現(xiàn)(implementation)兩部分組成箭阶,如果把一個模塊比喻成一個矩形,那么接口就是矩形頂部的邊戈鲁,而實現(xiàn)就是矩形的面積(也可以把實現(xiàn)看成是模塊提供的功能)仇参。當(dāng)一個模塊提供的功能一定時,深模塊(Deep module)的特點就是矩形頂部的邊比較短婆殿,整體形狀高瘦诈乒,也即接口比較簡單;淺模塊(Shallow module)的特點就是矩形頂部的邊比較長婆芦,整體形狀矮胖怕磨,也即接口比較復(fù)雜。

深模塊 VS 淺模塊

模塊的使用者往往只看到接口消约,模塊越深肠鲫,模塊暴露給調(diào)用者的信息就越少,調(diào)用者與該模塊的耦合性也就越低荆陆。因此滩届,把模塊設(shè)計得更“深”一點,有助于降低系統(tǒng)的復(fù)雜性被啼。

那么,怎樣才能設(shè)計出一個深模塊呢棠枉?

  • 更簡單的接口

    簡單的接口比簡單的實現(xiàn)更重要浓体,更簡單的接口意味著模塊的易用性更好,調(diào)用者使用起來更方便辈讶。而簡單的實現(xiàn) + 復(fù)雜的接口這種形式命浴,一方面影響了接口的易用性,另一方面則加深了調(diào)用者與模塊的耦合贱除。因此生闲,在進(jìn)行模塊設(shè)計時,最好遵守“把簡單留給別人月幌,把復(fù)雜留給自己”的原則碍讯。

    異常也屬于接口的一部分,在編碼過程中扯躺,應(yīng)該杜絕沒經(jīng)過處理捉兴,就隨意將異常往上拋的現(xiàn)象蝎困,這樣只會增加系統(tǒng)的復(fù)雜性。

  • 更通用的接口

    在設(shè)計接口時倍啥,你往往有兩種選擇:(1)設(shè)計成專用的接口禾乘;(2)設(shè)計成通用的接口。前者實現(xiàn)起來更方便虽缕,而且完全可以滿足當(dāng)前的需求始藕,但可擴(kuò)展性低,屬于戰(zhàn)術(shù)編程氮趋;后者則需要花時間對系統(tǒng)進(jìn)行抽象鳄虱,但可擴(kuò)展性高,屬于戰(zhàn)略編程凭峡。通用的接口意味著該接口適用的場景不止一個拙已,典型的就是“ 一個接口,多個實現(xiàn) ”的形式摧冀。

    有些程序員可能會反駁倍踪,在無法預(yù)知未來變化的情況下,通用就意味著過度設(shè)計索昂。過度通用確實屬于過度設(shè)計建车,但對接口進(jìn)行適度的抽象并不是,相反它可以使系統(tǒng)更有層次感椒惨,可維護(hù)性也更高缤至。

  • 隱藏細(xì)節(jié)

    在進(jìn)行模塊設(shè)計時,還要學(xué)會區(qū)分對于調(diào)用者而言康谆,哪些信息是重要的领斥,哪些信息是不重要的。隱藏細(xì)節(jié)指的就是只給調(diào)用者暴露重要的信息沃暗,把不重要的細(xì)節(jié)隱藏起來月洛。隱藏細(xì)節(jié)一則使模塊接口更簡單,二則使系統(tǒng)更易維護(hù)孽锥。

    如何判斷細(xì)節(jié)對于調(diào)用者是否重要嚼黔?以下有幾個例子:

    1. 對于Java的Map接口,重要的細(xì)節(jié)Map中每一個元素都是由<Key, Value>組成的惜辑;不重要的細(xì)節(jié)Map底層是如何存儲這些元素唬涧、如何實現(xiàn)線程安全等。
    2. 對于文件系統(tǒng)中的read函數(shù)盛撑,重要的細(xì)節(jié):每次讀操作從哪個文件讀碎节、讀多少字節(jié);不重要的細(xì)節(jié):如何切換到內(nèi)核態(tài)撵彻、如何從硬盤里讀數(shù)據(jù)等钓株。
    3. 對于多線程應(yīng)用程序实牡,重要的細(xì)節(jié):如何創(chuàng)建一個線程;不重要的細(xì)節(jié):多核CPU如何調(diào)度該線程轴合。

進(jìn)行分層設(shè)計创坞!

設(shè)計良好的軟件架構(gòu)都有一個特點,就是層次清晰受葛,每一層都提供了不同的抽象题涨,各個層次之間的依賴明確。不管是經(jīng)典的Web三層架構(gòu)总滩、DDD所提倡的四層架構(gòu)以及六邊形架構(gòu)纲堵,抑或是所謂的Clean Architecture,都有著鮮明的層次感闰渔。

Web三層架構(gòu)
DDD四層架構(gòu)
六邊形架構(gòu)
Clean Architecture

在進(jìn)行分層設(shè)計時席函,需要注意的是,每一層都應(yīng)該提供不同的抽象冈涧,并要盡量避免在一個模塊中出現(xiàn)大量的Pass-Through Mehod茂附。比如在DDD的四層架構(gòu)中,領(lǐng)域?qū)?/strong>提供了對領(lǐng)域業(yè)務(wù)邏輯的抽象督弓,應(yīng)用層提供了對系統(tǒng)用例的抽象营曼,接口層提供了對系統(tǒng)訪問接口的抽象,基礎(chǔ)設(shè)施層則提供對如數(shù)據(jù)庫訪問這類的基礎(chǔ)服務(wù)的抽象愚隧。

所謂的Pass-Through Mehod是指那些“在函數(shù)體內(nèi)直接調(diào)用其他函數(shù)蒂阱,而本身只做了極少的事情”的函數(shù),通常其函數(shù)簽名與被其調(diào)用的函數(shù)簽名很類似狂塘。Pass-Through Mehod所在的模塊通常都是淺模塊录煤,讓系統(tǒng)增加了無謂的層次和函數(shù)調(diào)用凶掰,會使系統(tǒng)更加復(fù)雜警没。

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

學(xué)會寫代碼注釋!

注釋是軟件開發(fā)過程中的性價比極高的一種手法搂根,它只需要花費20%的時間硝训,即可獲取80%的價值。它可以提高晦澀難懂的代碼的可讀性新思;可以起到隱藏代碼復(fù)雜細(xì)節(jié)的作用窖梁,比如接口注釋可以幫助開發(fā)者在沒有閱讀代碼的情況下快速了解該接口的功能和用法;如果寫的好夹囚,它還可以改善系統(tǒng)的設(shè)計纵刘。

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

總結(jié)

軟件的復(fù)雜性是我們程序員在日常開發(fā)中所必須面對的東西假哎,學(xué)會如何 “弄清楚什么是軟件復(fù)雜性瞬捕,找到導(dǎo)致軟件復(fù)雜的原因,并利用各種手法去戰(zhàn)勝軟件的復(fù)雜性” 是一門必備的能力舵抹。有句話說得很好肪虎,“代碼質(zhì)量決定生活質(zhì)量”,當(dāng)你把軟件的復(fù)雜性降低了惧蛹,bug減少了扇救,系統(tǒng)可維護(hù)性更高了,自然也就帶來了更好的生活質(zhì)量香嗓。

模塊設(shè)計是降低軟件復(fù)雜度最有效的手段迅腔,學(xué)會使用“戰(zhàn)略編程”的方法,并堅持下去靠娱。我們常常提倡“一次把事情做對”沧烈,但這對于模塊設(shè)計而言并不適用,幾乎沒有人可以第一次就把一個模塊設(shè)計成完美的模樣像云。二次設(shè)計是一個非常有效的手法锌雀,與其在系統(tǒng)腐化之后再花大量時間進(jìn)行重構(gòu)或重寫,還不如在第一次完成模塊設(shè)計后苫费,再花點時間進(jìn)行二次設(shè)計汤锨,多問問自己:是否有更簡單的接口是否有更通用的設(shè)計百框?是否有更簡潔高效的實現(xiàn)闲礼?

“羅馬不是一天建成的“,降低軟件的復(fù)雜性也一樣铐维,貴在堅持柬泽。

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市嫁蛇,隨后出現(xiàn)的幾起案子锨并,更是在濱河造成了極大的恐慌,老刑警劉巖睬棚,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件第煮,死亡現(xiàn)場離奇詭異,居然都是意外死亡抑党,警方通過查閱死者的電腦和手機(jī)包警,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來底靠,“玉大人害晦,你說我怎么就攤上這事∈钪校” “怎么了壹瘟?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵鲫剿,是天一觀的道長。 經(jīng)常有香客問我稻轨,道長灵莲,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任澄者,我火速辦了婚禮笆呆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘粱挡。我一直安慰自己赠幕,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布询筏。 她就那樣靜靜地躺著榕堰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嫌套。 梳的紋絲不亂的頭發(fā)上逆屡,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機(jī)與錄音踱讨,去河邊找鬼魏蔗。 笑死,一個胖子當(dāng)著我的面吹牛痹筛,可吹牛的內(nèi)容都是我干的莺治。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼帚稠,長吁一口氣:“原來是場噩夢啊……” “哼谣旁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起滋早,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤榄审,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后杆麸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搁进,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年昔头,在試婚紗的時候發(fā)現(xiàn)自己被綠了拷获。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡减细,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赢笨,到底是詐尸還是另有隱情未蝌,我是刑警寧澤驮吱,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站萧吠,受9級特大地震影響左冬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜纸型,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一拇砰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧狰腌,春花似錦除破、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至丹莲,卻和暖如春光坝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背甥材。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工盯另, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人洲赵。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓鸳惯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親板鬓。 傳聞我的和親對象是個殘疾皇子悲敷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容