軟體開發丨關於軟體重構的靈魂四問

amadan發表於2021-09-11

在軟體工程學中重構就是在不改變軟體現有功能的基礎上,透過調整程式程式碼改善軟體的質量、效能,使其程式的設計模式和架構更趨合理,提高軟體的擴充套件性和維護性。

摘要

在本文中,您會了解到如下的內容:

先新增新功能還是先進行重構?

重構到底有什麼價值?

如何評判這些價值?

重構的時機是什麼?

如何進行重構?

1. 先新增新功能還是先進行重構?

問題:

官方資料,重構分析1.0版中。

有兩頂帽子,一個是新增新功能,一個是重構

新增新功能時,你不應該修改既有程式碼,只管新增新功能,重構時你就不能再新增功能,只管改程式序結構。

一次只做一件事情。

這兩個是否有矛盾,以哪個為準?前面有些可信材料版本不一,有的還要互相打架,是否可以統一一下?

回覆:

關於新增新功能和重構是否矛盾的問題,是先新增新功能還是先進行重構?

我們要做的是觀察這兩個事情哪個更容易一些,我們要做更容易的那一個。

就是你不能一下子同時做這兩件事情。因為同時做兩件事情,會導致你工作的複雜度提升,容易出錯。

一般而言,重構會改變程式的設計結構改動相對來說比較大。但是因為沒有功能方面的新增,所以對應的測試案例我們不需要進行修改,那對我們來說,只要能夠使得現有的重構修改能夠滿足我們的業務測試案例就可以了。

新增新功能意味著我們要新增對應的測試案例,以保證我們新的功能是可測的。這部分的修改一般會依託現有的程式結構,改動起來相對比較少,並且修改容易鑑別。

在絕大多數正常情況下,我們一般是先新增功能,提交完成以後,再新的修改需求中對程式碼進行重構。

從大的方向上來說是分兩步走的,這兩個任務不能混為一談。

一次只做一件事情,一次提交只包含一個任務,這是為了避免在工作中人為的增加複雜度,這個複雜度包含程式碼修改,審查,測試等各個方面。

避免複雜度的上升,是我們在軟體開發過程中時刻要謹記的一個原則。

俗話說,一口吃不成胖子,心急吃不了熱豆腐。做事情要一步一個腳印,穩紮穩打,步步為營。

2. 重構的價值和評判效果

問題:

哪種型別的程式碼重構是高價值的?

1. 在網上跑了這麼多年也沒啥問題,為什麼要動他?

2. 重構前後功能又沒啥變化,當前收益是啥?

3. 若是提高可維護性,可擴充套件性的話,怎麼評判效果呢?

回覆:

這是關於重構價值和評判結果的問題。

這幾個問題問的都很好。

我們來看第1個問題,就是"在網上跑了這麼多年也沒啥問題,為什麼要動"的問題?

這裡的關鍵點就在於到底有沒有問題。是不是說在客戶那邊客戶看不到問題,就算是沒問題。

當然不是的,在我們軟體開發當中,在交付給客戶以後,客戶那邊看到的是黑盒,他不知道我們內部的邏輯存在多少的漏洞。

如果我們的內部邏輯存在很多的漏洞。假設偶然某一天,某個客戶發現了一個漏洞,它可以透過這一個漏洞進入到我們的系統內部,這樣進入我們的內部,會發生什麼樣的狀況,我們可以自己想象。

在公司的內部發言中專門提到了UK對我們產品的一個評價,外層是銅牆鐵壁,內層是很脆弱的,客戶或者駭客一旦進入到我們的內部以後,他就可以為所欲為了,從這一點上來說,我們一定要對我們現有的程式碼進行重構,以避免這樣的問題。

我們再來看第2個問題。重構前後功能又沒啥變化,當前收益是什麼?

重構最大的收益是解決如下的問題:

程式碼太多重複問題,單個函式體或者檔案或者攻城過大的問題,模組之間耦合度太高的問題等等。

以上問題歸根結底就是一個問題,就是複雜度過高的問題。

現在來談一談複雜度的問題,軟體開發中的複雜度當然是越低越好。一般談到複雜度,我們可能想到了各種邏輯上的複雜度,設計上的複雜度,實際上在軟體過程中複雜度涉及到方方面面,我們來看一下,具體有哪些方面我們需要注意複雜度的問題。

第一是命名規則。先舉個例子,我定一個變數叫word。有的人喜歡把它寫成wd。這個就增加了這個變數定義的複雜度,你從wd很難明白,這個變數是word的意思。

不管是變數的命名還是函式的命名,我們都希望看到名字,我們應該能夠理解這個變數或者函式大體是關聯到什麼樣子的事情。

所以謹慎的使用縮寫是避免命名規則複雜度提高的重要前提。

第二是程式邏輯的複雜度。線性順序執行的複雜度為1, 出現分支以後要乘以分支的個數。分支可以是條件判斷也可以是迴圈。所以儘可能的避免分支的出現是降低程式邏輯複雜度的重要手段。

如果程式分支不可避免,要儘可能的把程式分支放到最高的邏輯層。這樣做的目的是為了避免在下層處理的時候出現發散式的分支。發散式的分支會急劇的增加程式的複雜度。

複雜度越高,程式越難維護,複雜度超過一定程度,人類程式設計師是無法處理的。

第三是架構設計的複雜度。架構設計涉及到模組設計和系統設計。要儘可能的把一些公用的模組或者子系統抽取出來,比如安全相關的,日誌相關的,工具相關的等等,這些公用的功能可能會被所有其他的業務模組或系統所呼叫。

在呼叫這些公用功能的時候,越簡單越好,並且呼叫者不需要關心具體的內部實現,只需要知道如何使用就可以了。

這樣做的目的是讓程式設計師專注到業務程式碼的設計上來。

第四是系統部署的複雜度。系統部署包含幾個不同的階段如開發階段,測試階段和生產階段。不管是哪個階段,部署的步驟越少越不容易出錯。有些系統天然的需要很多指令的配置,如果是這樣的情況,需要編寫一個批處理的檔案來簡化外部使用者的部署步驟,把多個步驟變成一步。

與部署相關聯的還有整合部分。如果能夠實現自動化或者從模板中建立那是非常好的狀態。

第五是測試的複雜度。測試分白盒測試和黑盒測試。白盒測試的複雜度直接關聯著程式碼層級的複雜度,程式碼層級的複雜度越高,當然白盒測試的複雜度也就越高。

白盒測試需要注意的一個重要問題是不要使白盒測試這部分的程式碼脫離實際業務程式碼的設計。也就是說白盒測試它的依附物件就是我們實際的業務程式碼,從架構設計上說是一個附屬層,不要試圖在這裡使用什麼軟體設計藝術或者所謂的程式設計藝術。

這種程式碼的風格就是簡單直接,複雜度線性化。

黑盒測試的複雜度來自於業務需求分析。要有非常清晰的文件說明,需要對測試步驟和預期結果寫的非常清楚。

第六是技術的複雜度。技術的發展趨勢一般是越發展越簡單,功能越強大。那麼在設計和開發的過程中,要避免使用老舊的技術。關於技術框架的選擇,要提前做好調研。前端選什麼框架,要不要選擇某些UI庫,後端選什麼框架,要不要選擇某些程式庫,原則上是為了簡化我們的學習過程,提高開發效率,增強整個專案的可維護性。需要具體問題具體分析。

第七是隊伍結構的複雜度。隊伍構成一定要短小精悍,人多不一定好辦事。像亞馬遜提倡的是兩張披薩團隊,意思是說整個團隊兩張pizza就能吃飽。大體估算就是10人左右的一個隊伍。當然這只是一個參考指標。

整個隊伍的目標一定要明確。所有的人都向著那個目標邁進,分工可以不同,但是目標一定要一致。

目標+分工是隊伍成功運作的關鍵。具體來說就是把目標分成多個任務,每個任務裡又可以分成小任務,那所有的人都去做對應的任務,自己讓自己忙起來,而不是別人讓你忙起來。

我們現在來看一下第3個問題,就是如何評判重構效果的問題。在上面的分析中,我們已經瞭解了重構的目標和最大的收益,就是複雜度的降低。

那麼對應的,就是程式碼的重複率大大降低了,單個函式體或者程式碼檔案或者工程過大的問題不存在或者減少了,模組之間的耦合性降低了。

再進一步說,就是關於程式碼的可維護性和可擴充套件性上,我們需要關注這麼幾點:

一是程式碼的可讀性,我們看到現有的程式碼就應該可以理解程式碼作者的意圖是什麼,這樣我們在修改bug的時候就更容易把握。比如函式,類或者元件的功能要單一化,命名要友好,要刪除一些誤導性的註釋,對於一些沒用的程式碼,要毫不客氣的拋棄。

二是設計模式的可參考性。設計模式的好處就是提供一種可以追尋的程式碼擴充套件軌跡,新的功能可以遵循這種軌跡模板進行新增,從而獲得複雜度線性增長的效果。

三是白盒測試的完善性。儘管我們有非常強大的測試團隊,對於黑盒測試方面有很多的經驗和心得,但是現在我們有很多專案缺乏白盒測試案例,這使得開發者在進行重構的時候,面臨非常尷尬的境地。沒有充分的白盒測試案例,重構工作會舉步維艱,有一種瞎子摸象的感覺。

現在就說一下白盒測試這一部分。測試的框架應該在專案開始階段或者重構開始前搭起來。等部分程式碼成型的時候,逐步的新增必要的測試案例。測試案例的選取可以按照環形複雜度的計算方法來確定,也可以根據整合測試對應的使用者需求來確定。

與程式碼相關的測試,一般有單元測試,整合測試和系統級的測試。

單元測試,一般被認為非常繁瑣。單元測試的繁瑣主要體現在測試案例的選取上, 如果使用全覆蓋方式來選取測試案例的話,會產生大量的測試程式碼,以後維護起來也是一個負擔。如果採用環形複雜度來選取測試案例的話,會產生適量的測試程式碼,但是環形複雜度的計算也是一個很大的時間開銷。

整合測試跟客戶的實際業務需求相關。在這個過程中需要理清介面的輸入與輸出,以及執行路徑,然後據此來設計測試案例,寫出測試案例程式碼。

開發人員一般不會拒絕寫整合測試。因為她帶來的好處是實實在在的,會極大的提高你的開發效率和除錯效率。尤其是對於無介面的程式介面尤為重要。

系統級測試是大系統中子系統之間的整合測試。這個主要包含兩個方面:

一個方面是有介面的自動化測試,透過這樣的測試架構來模擬人類使用者的使用過程,同時增加一些隨機性的行為,試圖能夠找出系統的一些漏洞。

另一種是無介面的測試,體現在多個服務系統之間的呼叫上或者類似瀏覽器自動化框架的使用上。

一套完整的測試系統,可以幫助工程師提高開發效率,減少以後系統維護和重構的成本。

從測試的緊迫性上來說,整合測試最為必要,系統間的測試有時候使用手工測試透過一些測試工具來代替。單元測試可以有很廣闊的討論空間,這部分要具體問題具體分析。

3. 重構的時機

問題:

關於重構時機的說法,正確的是?

新增功能時,重構能夠使得未來新增特性時更快捷、更流暢

在修復錯誤時,應該聚焦問題本身,不建議重構,可以避免引入新的問題

專家Review時重構,能夠傳遞經驗,改善設計,避免或減少程式碼持續腐化

回覆:

關於重構的時機問題,現在我們有三個選項,我們就分別分析一下這三個選項。

第1個選項是說在新增功能的時候進行重構。這個選項的主要問題就是一個提交包含了多個任務。這屬於人為的增加工作的複雜度。第1個缺點是會增加工作的難度,使得本來可以用工作量1解決的問題,變成了工作量2和3。第2個缺點是增加了程式碼審查的難度。本來你的提交中描述的是新增功能,結果發現裡面的程式碼修改大部分與此描述無關。

所以第1個選項排除。

第2個選項是說在修復錯誤的時候應該聚焦問題本身,不建議重構,以避免引入新的問題。

聚焦是點睛之筆。我們在做任何事情的時候,都不要忘記初心,集中精力攻克問題,不要分心。

所以第2個選項是正確的。

第3個選項是說專家在審查程式碼的時候再重構。這裡面的最關鍵問題是專家可能並不瞭解程式碼的業務需求和應用場景。他們能夠看到程式碼存在不好的味道,但在不瞭解業務場景的情況下,讓專家進行重構會帶來很大的風險。

所以第3個選項也不正確。

4. 如何進行重構?

問題:

如何正確的進行重構?

回覆:

下面我們來看看如何進行重構。

簡單的程式碼重構我們都比較熟悉,比如說你透過工具就可以做一些整理,如變數重新命名,函式抽取,類建立等等。

現在比較頭疼的一個話題就是對老產品的重構,一些老產品涉及到上千萬行,上億行的程式碼。

關於老產品整改的問題。如果只是縫縫補補的話,可能起不到化繁為簡的目的。其實做類似這種工作的話,有一個比較可行的方案。就是把現有的產品當做一個成型系統也就是現有執行的產品,不要做大的改動,頂多就是修改bug。

然後以這些成型的系統為基準,去寫新的系統。相當於參照一個大的白盒就寫一個小的白盒,這樣新的小的白盒質量上肯定比大的白盒效能上要有優勢。

這樣子按部就班去做的話,就會比較靠譜。

有朋友會說上面的做法是重寫,字面意義上沒錯的。

實際上不矛盾。區別就是重構的方式應該從下往上還是從上往下。比如說我們現在大部分的重構都理解為從下往上來做。也就是感覺這個檔案裡頭有壞程式碼的味道,然後就改這個檔案,這樣做是沒有問題的。

比如現在有些教練遇到的問題,就是發現上下文不是很清晰,這個程式碼為什麼要這麼寫?為什麼一個檔案有1萬行或者3萬行,這個來龍去脈不是很清楚。

這個時候可能就需要從整個子模組來進行一個自上而下的分析。梳理出這個子模組的功能需求是怎樣的,需要有多少個公共介面?內部公共介面的實現方式是不是應該像目前這樣的?

一個檔案能夠寫成1萬行或者3萬行,肯定是有一定歷史原因的,絕大程度是由於全域性把握的程式設計能力不夠造成的。

像這種情況,如果從這個檔案本身去做重構的話,難度非常之大,但是如果從上往下,從模組的整個設計角度來做重構的話,可能就容易一些。

對於這樣的龐然大物,最好的辦法就是分而治之。首先要確定系統的功能邏輯點,針對這些邏輯點,要編排好對應的檢測點,也就是說等我們完成了重構以後,我們得確保我們的重構是沒有問題的,這些檢測點就是做這個的,我們可以理解成整合類的測試。

這些整合類的測試一定要確保可以在當前未重構之前的系統上正常執行。

有了這個設施以後,我們就可以開展我們的重構工作。重構的方法有很多,比如採用比較好的工具,函式和變數的命名改變,呼叫方式的改變等等。這些是在現有程式碼的基礎上進行的重構。這裡我們重點說一下重寫的方式來實現重構。所謂重寫呢,就是另外開闢一套程式碼底座。甚至可以選用不同的程式語言。

這種情況下重構首先要重用已有的業務邏輯,實現針對業務邏輯整合測試100%的透過率。

具體不管採用哪種方式都要一個模組一個模組的進行推進。驗證完成一個是一個,千萬不能急於求成,試圖一次性的把某些問題搞定。如果出現很多次失敗,有可能會消磨掉你的自信心。所以一定要一點一點的往前推進,始終是在進步當中。採用了這種方式以後,不管當前的系統有多麼的龐大,你只要堅持做下去,就一定能夠把重構工作徹底完成。

這個時候需要做的具體步驟可以參考如下:

1. 根據功能需求定義公共介面。

2. 根據公共介面寫出測試案例程式碼。

3. 這個時候可以按照測試驅動開發的理念去填充程式碼。

4. 程式碼可以從現有的程式碼中抽取出來。

5. 在抽取的過程中進行整理重構。

這樣,這個子模組完成以後,就可以嘗試去替代現有的子模組,看看能不能在整個系統中安全的執行。

對於整個系統來說,我們又可以分成很多個子模組。然後又可以對各個子模組各個擊破,最終完成對整個系統的重構。

如果一開始對整個系統進行重構的話,也是可以從自上而下的角度來看的。

比如說開始的時候先把所有的子模組看成一些佔位符,假定他們已經完成他們的介面了。那對於整個系統來說,它本身就是一個子模組,屬於提綱挈領的那個模組。

這個過程,從字面意義上可以理解成重寫,實際上,它也是一個重構的過程,因為我們肯定會重用這個系統本身的一些現有程式碼和現有的邏輯。

上面我們是假定系統在已經完成的情況下進行的重構,其實重構可以貫穿於軟體開發的始終。軟體開發的首要目標是實現業務邏輯,能夠解決客戶的問題。這個目標實現以後,我們就要追求程式碼的乾淨度,複雜度能夠降到最小,當前的技術能夠用到最先進。

所以只要有機會,我們都應該對程式碼和設計進行重構。

結語

本文針對收到的幾個關於重構方面的問題作了回答,側重點各不一樣,希望能夠給存在相同困惑的朋友們有所啟示。

 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2310/viewspace-2796452/,如需轉載,請註明出處,否則將追究法律責任。

相關文章