# CH2--重構原則 <!-- {%hackmd theme-dark %} --> ## 重構的定義 重構 (refactoring): * 對於軟體內部結構所做的修改/變更,讓內部結構更容易理解和修改;這些修改/變更不會影響軟體的外顯行為 (或是使用者能感受到的行為)。 * 實施一連串的修改/變更來修改軟體內部結構,而不影響其外顯行為。 - 重構 !== 重新架構 (restructuring) 重新架構 == 重新組織或清理 code base - 重構 !== 效能優化 (performance optimization) 兩者有不同的目的: * 效能優化:提高軟體效能;程式碼可能變得更難讀 * 重構:讓程式碼更容易理解和修改;可能優化或降低效能 ## 為什麼應該重構? - __重構能夠改善軟體設計__ 設計較差的軟體通常會在多處出現相似功能的程式碼。 重構能減少此類冗贅的程式碼,減少未來開發者要去理解的程式碼份量,減少修改錯誤的機會。 - __重構能讓軟體程式碼更好理解__ 重構會讓程式碼更好閱讀,接手的開發人員及開發者本人都會更快進入狀況。 - __重構能讓 bug 更容易發現__ 重構時,開發者需要深入了解手上處理的程式碼,有助減少在理解程式碼時的猜想/假設,更容易找到潛藏的 bug。 - __重構能夠加速程式開發__ 進行重構需要額外時間,但是能夠改善軟體品質--更佳的程式碼架構、可讀性及更少的 bug。未來「新增功能」所需的時間會更短。 如果不進行重構,雖然一開始能夠快速開發,但是程式碼品質不佳,將會導致未來新增功能時要花更多時間處理。 ![](https://i.imgur.com/vjAlshR.png) ## 何時應該重構? 作者:寫程式時,大約每小時會重構一次 三次法則 (The Rule of Three):當類似的程式碼寫了第三次時,試著重構。 - __Preparatory refactoring-- 新增功能更容易__ 重構的最佳時機:在加入新功能之前 先閱讀現有的程式碼,判斷現有架構是否需要微調 (微調後能不能讓任務更順利進行)、是否已有類似功能的程式碼可使用,再著手進行。 - __Comprehension refactoring -- 讓程式碼更好理解__ 研讀程式碼,確認現有程式碼是否容易理解 重新命名變數、拆分函式 => 架構會開始浮現 - __Litter-pickup refactoring__ 了解既有程式碼後,如果發現程式碼中有寫得不好的地方 (例如,邏輯過份複雜),就把不好的地方處理。 - __Planned vs opportunistic refactoring__ preparatory | comprehension | litter-pickup 重構都是看到重構需求時再進行。 重構是新增功能或修正 bug 過程中的一部份,並不會特別排出一段時間專門進行重構。 遇見品質不佳的程式碼時就要進行重構;即使是品質優良的程式碼也需要經常重構,才能適應新的需求。 如果平時不進行重構,的確可能需要排定時間進行重構。即使是經常重構的程式碼,到了某個階段,也可能需要排定時間重構,但機率並不高。因此,絕大部份的重構應該在看見需求時就進行處理。 - __長時間重構 (long-term refactoring)__ 有些重構需要花費數星期才能完成,例如更換函式庫、將程式碼抽取成元件等等 即使在這樣的情況下,還是不贊成排出特定時間進行重構。建議當團隊成員處理到相關範圍的程式碼時,再採用小步驟處理,確保程式碼不會被破壞。 - __程式碼審查 (code review) 時的重構__ 程式碼審查可以讓團隊成員確認彼此的程式碼是否容易理解。 審查者可以利用重構,給予對方明確的建議。 進行審查時,最好讓作者和審查者透過 pair programming 的方式進行:作者提供撰寫程式碼時的上下文資訊,審查者進行重構。這樣一來,作者比較能夠理解審查者提出的修改和背後的意圖。 - __怎麼跟管理者說明重構的重要性__ 對於了解技術的管理者來說,不需要太大力氣就能說服。 對於不了解技術或只在乎時程的管理者,不要跟他們提出這項需求。只要依自己的專業判斷在工作時進行重構就好。 - __何時不該進行重構?__ 不會碰到/不需要了解的程式碼,即使再混亂,也不用去重構。如果重構有益,才去重構。 如果重寫程式碼比重構更容易的話,也不用重構。 ## 重構會遇到的問題 - __拖慢新增功能的速度__ 重構看似會拖慢加入新功能的時程,但是重構會讓 code base 更健全,反而會讓新功能更容易實作出來。 在某些情況下,仍要以自己的專業考慮重構或不重構/延後重構的利弊得失: * 要加入的新功能極小,而重構幅度頗大 => 先加入新功能,重構之後再做 * 很少碰到的程式碼或是不常造成問題的程式碼 => 不太會重構 * 不確定該如何重構程式碼 => 暫緩進行重構 進行重構是因為它能帶來經濟效益:更快加入新功能、更快修復 bug。 不要單純因為道德理由 (例如,重構就是好習慣或重構才是好工程師) 而執意進行重構。 - __程式碼所有權__ 如果重構時需要處理到其他團隊/第三方服務提供的程式碼,就可能受到限制。 不贊成將程式碼所有權過度精細切分到不同工程師身上,而且只限該工程師能夠修改。 贊成以團隊為單位,團隊成員皆有權利修改該團隊負責的程式碼。 延伸到跨團隊合作情景:B 團隊能夠從 A 團隊的程式碼建立分支,在分支裡提出修改,讓 A 團隊審查是否要合併。 - __分支__ 進行版本控制時,慣例作法是拆分 main 分支和 feature 分支 (用來加入新功能),feature 分支完成後再併入 main 分支。 * 優點: * 確保 main 分支乾淨、沒有正在處理中的程式碼 * 有各項新功能的歷史紀錄 * 當新加入的功能產生問題時,能夠回復到先前版本 * 缺點: * 當單一 feature 分支處理時間愈長,併入 main 分支時會更加不易;特別是同時有多個 feature 分支在處理的狀況。 * 版本控制系統無法察覺語意變化: 1. 在 A 分支,程式碼呼叫 fa 函式。 2. 在 B 分支,fa 函式改名為 fb (發生語意變化)。 3. 合併分支時,A 分支內呼叫 fa 函式的地方會出錯 (因為 fa 已被改成 fb)。 * 解法: * 採用持續整合 (continuous integration,CI):縮短 feature 分支的處理時間為一至數日,完成後儘快合併。確保分支之間差異不致太大,減少合併時的複雜度。CI 與重構相容性高,都以多次、小幅度更新的方式來處理程式碼。 * 若不採用完整的 CI 流程,則要經常合併程式碼 - __測試__ 擁有覆蓋率高的測試,可以確保重構和加入新功能時沒有發生錯誤。 - __遺留程式碼 (legacy code)__ 如果遺留程式碼裡沒有測試,無法安全進行重構。若要重構,則必須加入測試。 若要在遺留程式碼內加入測試,同時必須進行重構:將程式碼重構成能夠測試的狀態。然而,這很可能引入 bug,卻也是必須擔負的風險。作法可參考 Working Effectively with Legacy Code (重構遺留程式碼的藝術)。 - __資料庫__ 重構資料庫時,建議將修改拆分到多個正式站的 release 內。這樣一來,在正式站發生問題時,就可以回復到之前的狀態。 ## 重構、軟體架構與 Yagni 過往認為,軟體需求可以在早期階段了解,因此軟體設計和架構應該在開發前就訂定好。 然而,現實是透過使用回饋才能更清楚了解需求。 一開始先以能夠處理當前問題的架構來撰寫程式碼,不預先設想未來需求 (You Aren't Gonna Need It,YAGNI)。之後再透過重構回應新的需求,如此一來可以持續改變現有程式碼的設計,更能順利適應新功能。好的前期規劃不可或缺,但重構的影響更為重大。 ## 重構與軟體開發流程 敏捷程式開發除了需要引入重構,也需要引入持續整合 (確保產品隨時處於可 release 的狀態) 和測試 (確保程式正常運作)。 ## 重構與效能 重構很可能讓程式效能變慢,但是程式的效能會更容易調整。 三種效能調整的處理方式: * __Time budgeting (編列時間預算)__:為每個元件分配可用的時間,但僅適合硬即時系統 (hard real-time system),例如心律調節器;但不適合其他系統,例如企業資訊系統。 * __constant attention (持續關注)__:每位開發者不斷盡其所能地讓軟體保持高效能。然而,為了讓軟體保持高效能所做的修改,通常會讓程式碼難讀、難處理,反而減慢開發速度。此外,執行此類效能調整時,對於程式行為的思考較為狹隘,也可能對編譯器、runtime 及硬體行為帶有誤解。 * __90-percent statistic (90% 統計)__:建立架構拆分清楚的程式,只需一路專心打造軟體。等到要刻意調節效能時,再用 profiler 檢測何處使用較多執行時間和儲存空間。再針對效能較差的地方進行調整。效能較差的地方通常和開發者想像的不同,因此需要用 profiler 確認。 ## 自動化重構工具 基本功能:利用搜尋、取代等字串處理功能。一般編輯器可做到。 更可靠的功能:利用程式碼的句法樹 (syntax tree) 來搜尋程式碼、linting 及執行其他功能。IDE 有辦法做到。 ![](https://i.imgur.com/IE3poSR.png) https://www.skovy.dev/blog/introducing-practical-abstract-syntax-trees?seed=fqe6ex