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

在軟件工程學中重構就是在不改變軟件現有功能的基礎上,經過調整程序代碼改善軟件的質量、性能,使其程序的設計模式和架構更趨合理,提升軟件的擴展性和維護性。前端

摘要程序員

在本文中,您會了解到以下的內容:編程

先添加新功能仍是先進行重構?後端

重構到底有什麼價值?設計模式

如何評判這些價值?瀏覽器

重構的時機是什麼?安全

如何進行重構?架構

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

這樣,這個子模塊完成之後,就能夠嘗試去替代現有的子模塊,看看能不能在整個系統中安全的運行。

對於整個系統來講,咱們又能夠分紅不少個子模塊。而後又能夠對各個子模塊各個擊破,最終完成對整個系統的重構。

若是一開始對整個系統進行重構的話,也是能夠從自上而下的角度來看的。

好比說開始的時候先把全部的子模塊當作一些佔位符,假定他們已經完成他們的接口了。那對於整個系統來講,它自己就是一個子模塊,屬於提綱挈領的那個模塊。

這個過程,從字面意義上能夠理解成重寫,實際上,它也是一個重構的過程,由於咱們確定會重用這個系統自己的一些現有代碼和現有的邏輯。

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

因此只要有機會,咱們都應該對代碼和設計進行重構。

結語

本文針對收到的幾個關於重構方面的問題做了回答,側重點各不同,但願可以給存在相同困惑的朋友們有所啓示。

 

點擊關注,第一時間瞭解華爲雲新鮮技術~