天天看點

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

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

摘要

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

先添加新功能還是先進行重構?

重構到底有什麼價值?

如何評判這些價值?

重構的時機是什麼?

如何進行重構?

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. 在抽取的過程中進行整理重構。

這樣,這個子子產品完成以後,就可以嘗試去替代現有的子子產品,看看能不能在整個系統中安全的運作。

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

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

比如說開始的時候先把所有的子子產品看成一些占位符,假定他們已經完成他們的接口了。那對于整個系統來說,它本身就是一個子子產品,屬于提綱挈領的那個子產品。

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

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

是以隻要有機會,我們都應該對代碼和設計進行重構。

結語

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

點選關注,第一時間了解華為雲新鮮技術~

繼續閱讀