淺談如何降低軟件復(fù)雜性
前言
在進(jìn)行軟件開(kāi)發(fā)時(shí),我們常常會(huì)追求軟件的高可維護(hù)性,高可維護(hù)性意味著當(dāng)有新需求來(lái)時(shí),系統(tǒng)易擴(kuò)展;當(dāng)出現(xiàn)bug時(shí),開(kāi)發(fā)人員易定位。而當(dāng)我們說(shuō)一個(gè)系統(tǒng)的可維護(hù)性太差時(shí),往往指的是該系統(tǒng)太過(guò)復(fù)雜,導(dǎo)致給系統(tǒng)增加新功能時(shí)容易出現(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.
可見(jiàn),軟件的復(fù)雜性是一個(gè)很泛的概念,任何使軟件難以理解和難以修改的東西,都屬于軟件的復(fù)雜性。為此,John Ousterhout提出了一個(gè)公式來(lái)度量一個(gè)系統(tǒng)的復(fù)雜性:
公式中,表示系統(tǒng)中的模塊,表示該模塊的認(rèn)知負(fù)擔(dān)(Cognitive Load,即一個(gè)模塊難以理解的程度),表示在日常開(kāi)發(fā)中在該模塊花費(fèi)的開(kāi)發(fā)時(shí)間。
從公式上看,一個(gè)軟件的復(fù)雜性由它的各個(gè)模塊的復(fù)雜性累加而成,而模塊復(fù)雜性 = 模塊認(rèn)知負(fù)擔(dān) * 模塊開(kāi)發(fā)時(shí)間,也就是模塊的復(fù)雜性即和模塊本身有關(guān),也跟在該模塊上花費(fèi)的開(kāi)發(fā)時(shí)間有關(guān)。需要注意的是,如果一個(gè)模塊非常難以理解,但是后續(xù)開(kāi)發(fā)過(guò)程中幾乎沒(méi)有涉及到它,那么它的復(fù)雜性也是很低的。
導(dǎo)致軟件復(fù)雜的原因
導(dǎo)致軟件復(fù)雜的原因可以細(xì)分出很多種來(lái),而概括起來(lái)莫過(guò)于兩種:依賴(lài)(dependencies)和隱晦(obscurity)。前者會(huì)讓修改起來(lái)很費(fèi)勁而且容易出現(xiàn)bug,比如當(dāng)修改模塊1時(shí),往往也涉及到模塊2、模塊3、... 的改動(dòng);后者會(huì)讓軟件難以理解,定位一個(gè)bug,甚至是僅僅讀懂一段代碼都需要花費(fèi)大量的時(shí)間。
軟件的復(fù)雜性往往伴隨著如下幾種癥狀:
霰彈式修改(Change amplification)。當(dāng)只需要修改一個(gè)功能,但又不得不對(duì)許多模塊作出改動(dòng)時(shí),我們稱(chēng)之為霰彈式修改。這通常是因?yàn)槟K之間耦合過(guò)重,相互依賴(lài)太多導(dǎo)致的。 比如,有一組Web頁(yè)面,每個(gè)頁(yè)面都是一個(gè)HTML文件,每個(gè)HTML都有一個(gè)背景屬性。由于各個(gè)HTML的背景屬性都是分開(kāi)定義的,因此如果需要把背景顏色從橙色修改為藍(lán)色時(shí),就需要改動(dòng)所有的HTML文件。
霰彈式修改的典型例子
認(rèn)知負(fù)擔(dān)(Cognitive load)。當(dāng)我們說(shuō)一個(gè)模塊隱晦、難以理解時(shí),它就有過(guò)重的認(rèn)知負(fù)擔(dān),這種情況下往往需要讀者花費(fèi)大量時(shí)間才能明白該模塊的功能。比如,提供一個(gè)不帶任何注釋的calculate接口,它有2個(gè)int類(lèi)型的入?yún)⒑鸵粋€(gè)int類(lèi)型的返回值。從該函數(shù)的簽名上看,調(diào)用者根本無(wú)法得知函數(shù)的功能是什么,他只能通過(guò)花時(shí)間去閱讀源碼來(lái)確定函數(shù)功能后才敢去調(diào)用該函數(shù)。
int calculate(int val1, int val2);
不確定性(Unknown unknowns)。相比于前兩種癥狀,不確定性的破壞性更大,它通常指一些在開(kāi)發(fā)需求時(shí),你必須注意的,但是又無(wú)從得知的點(diǎn)。它常常是因?yàn)橐恍╇[晦的依賴(lài)導(dǎo)致的,會(huì)讓你在開(kāi)發(fā)完一個(gè)需求之后感覺(jué)心里很沒(méi)譜,隱約覺(jué)得自己的代碼哪里有問(wèn)題,但又不清楚問(wèn)題在哪,只能祈禱在測(cè)試階段能夠暴露而不要漏洞商用階段。
如何降低軟件的復(fù)雜性
對(duì) “戰(zhàn)術(shù)編程” Say No!
很多程序員在進(jìn)行特性開(kāi)發(fā)或bug修復(fù)時(shí),關(guān)注點(diǎn)往往是如何簡(jiǎn)單快速讓程序跑起來(lái),這就是典型的戰(zhàn)術(shù)編程(Tactical programming)方法,它追求的是短期的效益——節(jié)省開(kāi)發(fā)時(shí)間。戰(zhàn)術(shù)編程最普遍的體現(xiàn)就是在編碼之前沒(méi)有進(jìn)行模塊設(shè)計(jì),想到哪里就寫(xiě)到哪里。戰(zhàn)術(shù)編程在系統(tǒng)前期可能會(huì)比較方便,一旦系統(tǒng)龐大起來(lái)、模塊之間的耦合變重之后,添加或修改功能、修復(fù)bug都會(huì)變得寸步難行。隨著系統(tǒng)變得越來(lái)越復(fù)雜,最后不得不對(duì)系統(tǒng)進(jìn)行重構(gòu)甚至重寫(xiě)。
與戰(zhàn)術(shù)編程相對(duì)的就是戰(zhàn)略編程(Strategic programming),它追求的是長(zhǎng)期的效益——增加系統(tǒng)可維護(hù)性。僅僅是讓程序跑起來(lái)還不足以滿(mǎn)足,還需要考慮程序的可維護(hù)性,讓后續(xù)在添加或修改功能、修復(fù)bug時(shí)都能夠快速響應(yīng)。因?yàn)榭紤]的點(diǎn)比較多,也就注定戰(zhàn)略編程需要花費(fèi)一定的時(shí)間去進(jìn)行模塊設(shè)計(jì),但相比于戰(zhàn)術(shù)編程后期導(dǎo)致的問(wèn)題,這一點(diǎn)時(shí)間也是完全值得的。
戰(zhàn)術(shù)編程 VS 戰(zhàn)略編程
讓模塊更“深”一點(diǎn)!
一個(gè)模塊由接口(interface)和實(shí)現(xiàn)(implementation)兩部分組成,如果把一個(gè)模塊比喻成一個(gè)矩形,那么接口就是矩形頂部的邊,而實(shí)現(xiàn)就是矩形的面積(也可以把實(shí)現(xiàn)看成是模塊提供的功能)。當(dāng)一個(gè)模塊提供的功能一定時(shí),深模塊(Deep module)的特點(diǎn)就是矩形頂部的邊比較短,整體形狀高瘦,也即接口比較簡(jiǎn)單;淺模塊(Shallow module)的特點(diǎn)就是矩形頂部的邊比較長(zhǎng),整體形狀矮胖,也即接口比較復(fù)雜。
深模塊 VS 淺模塊
模塊的使用者往往只看到接口,模塊越深,模塊暴露給調(diào)用者的信息就越少,調(diào)用者與該模塊的耦合性也就越低。因此,把模塊設(shè)計(jì)得更“深”一點(diǎn),有助于降低系統(tǒng)的復(fù)雜性。
那么,怎樣才能設(shè)計(jì)出一個(gè)深模塊呢?
更簡(jiǎn)單的接口
簡(jiǎn)單的接口比簡(jiǎn)單的實(shí)現(xiàn)更重要,更簡(jiǎn)單的接口意味著模塊的易用性更好,調(diào)用者使用起來(lái)更方便。而簡(jiǎn)單的實(shí)現(xiàn) + 復(fù)雜的接口這種形式,一方面影響了接口的易用性,另一方面則加深了調(diào)用者與模塊的耦合。因此,在進(jìn)行模塊設(shè)計(jì)時(shí),最好遵守“把簡(jiǎn)單留給別人,把復(fù)雜留給自己”的原則。
異常也屬于接口的一部分,在編碼過(guò)程中,應(yīng)該杜絕沒(méi)經(jīng)過(guò)處理,就隨意將異常往上拋的現(xiàn)象,這樣只會(huì)增加系統(tǒng)的復(fù)雜性。
更通用的接口
在設(shè)計(jì)接口時(shí),你往往有兩種選擇:(1)設(shè)計(jì)成專(zhuān)用的接口;(2)設(shè)計(jì)成通用的接口。前者實(shí)現(xiàn)起來(lái)更方便,而且完全可以滿(mǎn)足當(dāng)前的需求,但可擴(kuò)展性低,屬于戰(zhàn)術(shù)編程;后者則需要花時(shí)間對(duì)系統(tǒng)進(jìn)行抽象,但可擴(kuò)展性高,屬于戰(zhàn)略編程。通用的接口意味著該接口適用的場(chǎng)景不止一個(gè),典型的就是“一個(gè)接口,多個(gè)實(shí)現(xiàn)”的形式。
有些程序員可能會(huì)反駁,在無(wú)法預(yù)知未來(lái)變化的情況下,通用就意味著過(guò)度設(shè)計(jì)。過(guò)度通用確實(shí)屬于過(guò)度設(shè)計(jì),但對(duì)接口進(jìn)行適度的抽象并不是,相反它可以使系統(tǒng)更有層次感,可維護(hù)性也更高。
隱藏細(xì)節(jié)
在進(jìn)行模塊設(shè)計(jì)時(shí),還要學(xué)會(huì)區(qū)分對(duì)于調(diào)用者而言,哪些信息是重要的,哪些信息是不重要的。隱藏細(xì)節(jié)指的就是只給調(diào)用者暴露重要的信息,把不重要的細(xì)節(jié)隱藏起來(lái)。隱藏細(xì)節(jié)一則使模塊接口更簡(jiǎn)單,二則使系統(tǒng)更易維護(hù)。
如何判斷細(xì)節(jié)對(duì)于調(diào)用者是否重要?以下有幾個(gè)例子:
1、對(duì)于Java的Map接口,重要的細(xì)節(jié):Map中每一個(gè)元素都是由<Key, Value>組成的;不重要的細(xì)節(jié):Map底層是如何存儲(chǔ)這些元素、如何實(shí)現(xiàn)線(xiàn)程安全等。
2、對(duì)于文件系統(tǒng)中的read函數(shù),重要的細(xì)節(jié):每次讀操作從哪個(gè)文件讀、讀多少字節(jié);不重要的細(xì)節(jié):如何切換到內(nèi)核態(tài)、如何從硬盤(pán)里讀數(shù)據(jù)等。
3、對(duì)于多線(xiàn)程應(yīng)用程序,重要的細(xì)節(jié):如何創(chuàng)建一個(gè)線(xiàn)程;不重要的細(xì)節(jié):多核CPU如何調(diào)度該線(xiàn)程。
進(jìn)行分層設(shè)計(jì)!
設(shè)計(jì)良好的軟件架構(gòu)都有一個(gè)特點(diǎn),就是層次清晰,每一層都提供了不同的抽象,各個(gè)層次之間的依賴(lài)明確。不管是經(jīng)典的Web三層架構(gòu)、DDD所提倡的四層架構(gòu)以及六邊形架構(gòu),抑或是所謂的Clean Architecture,都有著鮮明的層次感。
在進(jìn)行分層設(shè)計(jì)時(shí),需要注意的是,每一層都應(yīng)該提供不同的抽象,并要盡量避免在一個(gè)模塊中出現(xiàn)大量的Pass-Through Mehod。比如在DDD的四層架構(gòu)中,領(lǐng)域?qū)犹峁┝藢?duì)領(lǐng)域業(yè)務(wù)邏輯的抽象,應(yīng)用層提供了對(duì)系統(tǒng)用例的抽象,接口層提供了對(duì)系統(tǒng)訪(fǎng)問(wèn)接口的抽象,基礎(chǔ)設(shè)施層則提供對(duì)如數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)這類(lèi)的基礎(chǔ)服務(wù)的抽象。
所謂的Pass-Through Mehod是指那些“在函數(shù)體內(nèi)直接調(diào)用其他函數(shù),而本身只做了極少的事情”的函數(shù),通常其函數(shù)簽名與被其調(diào)用的函數(shù)簽名很類(lèi)似。Pass-Through Mehod所在的模塊通常都是淺模塊,讓系統(tǒng)增加了無(wú)謂的層次和函數(shù)調(diào)用,會(huì)使系統(tǒng)更加復(fù)雜。
Pass-Through Mehod(選自《A Philosophy of Software Design》中的例子)
學(xué)會(huì)寫(xiě)代碼注釋?zhuān)?/h3>
注釋是降低軟件復(fù)雜性的性?xún)r(jià)比極高的一種手法,它只需要花費(fèi)20%的時(shí)間,即可獲取80%的價(jià)值。它可以提高晦澀難懂的代碼的可讀性;可以起到隱藏代碼復(fù)雜細(xì)節(jié)的作用,比如接口注釋可以幫助開(kāi)發(fā)者在沒(méi)有閱讀代碼的情況下快速了解該接口的功能和用法;如果寫(xiě)的好,它還可以改善系統(tǒng)的設(shè)計(jì)。
具體如何寫(xiě)好代碼注釋?zhuān)瑓⒖肌督棠銓?xiě)好代碼注釋》一文。
總結(jié)
軟件的復(fù)雜性是我們程序員在日常開(kāi)發(fā)中所必須面對(duì)的東西,學(xué)會(huì)如何 “弄清楚什么是軟件復(fù)雜性,找到導(dǎo)致軟件復(fù)雜的原因,并利用各種手法去戰(zhàn)勝軟件的復(fù)雜性” 是一門(mén)必備的能力。有句話(huà)說(shuō)得很好,“代碼質(zhì)量決定生活質(zhì)量”,當(dāng)你把軟件的復(fù)雜性降低了,bug減少了,系統(tǒng)可維護(hù)性更高了,自然也就帶來(lái)了更好的生活質(zhì)量。
模塊設(shè)計(jì)是降低軟件復(fù)雜度最有效的手段,學(xué)會(huì)使用“戰(zhàn)略編程”的方法,并堅(jiān)持下去。我們常常提倡“一次把事情做對(duì)”,但這對(duì)于模塊設(shè)計(jì)而言并不適用,幾乎沒(méi)有人可以第一次就把一個(gè)模塊設(shè)計(jì)成完美的模樣。二次設(shè)計(jì)是一個(gè)非常有效的手法,與其在系統(tǒng)腐化之后再花大量時(shí)間進(jìn)行重構(gòu)或重寫(xiě),還不如在第一次完成模塊設(shè)計(jì)后,再花點(diǎn)時(shí)間進(jìn)行二次設(shè)計(jì),多問(wèn)問(wèn)自己:是否有更簡(jiǎn)單的接口?是否有更通用的設(shè)計(jì)?是否有更簡(jiǎn)潔高效的實(shí)現(xiàn)?
"羅馬不是一天建成的",降低軟件的復(fù)雜性也一樣,貴在堅(jiān)持。
以上就是淺談如何降低軟件復(fù)雜性的詳細(xì)內(nèi)容,更多關(guān)于如何降低軟件復(fù)雜性的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解基于深度學(xué)習(xí)的兩種信源信道聯(lián)合編碼
信源編碼是一個(gè)數(shù)據(jù)壓縮的過(guò)程,其目的是盡可能地將信源中的冗余度去掉;而信道編碼則是一個(gè)增加冗余的過(guò)程,通過(guò)適當(dāng)加入冗余度來(lái)達(dá)到抵抗信道噪聲,保護(hù)傳輸數(shù)據(jù)的目的。2021-05-05Eclipse 誤刪文件的恢復(fù)與代碼的恢復(fù)詳解
這篇文章主要介紹了Eclipse 誤刪文件的恢復(fù),代碼的恢復(fù)的相關(guān)資料,需要的朋友可以參考下2016-09-09ffmpeg播放器實(shí)現(xiàn)詳解之框架搭建過(guò)程
這篇文章主要介紹了ffmpeg播放器實(shí)現(xiàn)詳解之框架搭建過(guò)程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07好玩又實(shí)用的查看函數(shù)圖像網(wǎng)站Desmos
這個(gè)網(wǎng)站的最大優(yōu)點(diǎn),就是省去了安裝數(shù)學(xué)繪圖軟件或計(jì)算軟件的麻煩,只要打開(kāi)瀏覽器就能使用了??戳私榻B之后,可別忘了把這個(gè)好網(wǎng)站加到書(shū)簽2021-08-08