從事程式開發超過二十年,我一直不是開發方法論的信徒,對於 TDD、重構、Pair Programming 這些僅止於概略輪廓,不曾深入。不過,寫了超過一萬小時,對於程式該怎麼寫才漂亮,多少也發展出自己的心法,例如:抽取成方法重複利用或共用、將函數結果存成變數重複利用減少呼叫... 等等。不記得是怎麼學會這種做法,也不懂招式名稱,但其實他們都是「重構」(Refactoring)這門學問裡常用的基本手法,叫做 Extract Function、Extract Variable。

前陣子聽說敏捷開發大師 Martin Fowler 的大作「 重構」已出到第二版且有中譯本,雖然不熱衷此道,但要在江湖走跳,增廣見聞確有必要,抱著朝聖的心情入手一本拜讀。

讀了第一章,大驚! 它顛覆我多年來對「好程式」的認知,帶來震憾。歷經一番思考消化,對於「程式好壞」這件事,我有了不一樣新的體認。

寫了幾十年程式,我心中有一些奉為圭臬的鐵律,其中蠻大比例基於效能考量,例如:

  • 讀取變數比呼叫函式節省資源,故將函式執行結果存成變數重複利用可提升效能
  • 反覆執行的作業,應該要設法合併用最少次數做完

每次看到有人違反這些原則,我的直覺反應是「靠,你會不會寫程式?」

在重構的第一章,Martin Fowler 用了一個劇團發票 JavaScript 程式當範例,展示如何重構,其中有兩招:

原本呼叫 playFor(perf) 將結果存入 play 重複使用三次,重構成每次都直接呼叫 playFor() 函式:

原本跑一次 for 迴圈同時計算總金額及紅利積點,拆成兩個迴圈,計算總金額跑一次,計算紅利積點跑一次:

乍讀到這段,心裡浮現滿滿的花惹發! 看完大師示範,覺得自己都不會寫程式了,這是本讓人走火入魔的邪書呀! 心魔既成,後面章節變得難以下嚥,頓失繼續讀下去的動力。

為了打破心魔,向重構專家 91 哥請益,得到一些提點,加上自己反覆琢磨思索,我對於「好程式」有了新認識。

過去我對「好程式」的定義可納歸成一個核心原則 - 極力消滅「重複」,減少浪費。避免相似邏輯的程式碼重複出現(未來修改要改多處),避免反覆呼叫、反覆執行,次數愈少愈有效率。 而 Fowler 有句名言:

任何一個傻瓜都能寫出電腦可以理解的程式,唯有優秀的程式設計師能寫出讓人讀懂的程式。 M. Fowler (1999)

程式執行效率很重要,但好理解容易維護也很重要,很多時候二者無法兼顧,就必須取捨。至於何者為重?以 Fowler 的觀點,當然是後者。

反思多年的開發實際經驗,不得不認同重構精神所主張的 - 程式易讀好維護產生的效益遠大於執行效能。以上面的金額、積點跑兩次迴圈為例,以當今的硬體能力,拆成兩個迴圈增加的時間或許不及 1µs,跑 100 萬次只會多 1 秒。但拆成兩個迴圈有利於後續將邏輯拆解成函式,再進一步實現模組化、物件導向的設計架構。後人接手、修改可節省的時間以小時、以天甚至以月計(更不用提減少程式改壞風險可降低的成本),回頭檢視違反效能最佳化所付的代價,渺小到可以直接忽略。當然,一定有特例我們必須以效能考量優先,放棄方便維護的寫法。對於重構與效能的取捨,Fowler 是這麼建議的:

如果你真的去測量重構前後程式執行時間,會發現差異不大。大部分程式人員(甚至是經驗豐富的老鳥)都無法準確判斷程式寫法的快慢,我們的許多直覺早已被聰明的 Compiler、新一代快取等技術推翻。軟體性能關鍵往往只取決於瓶頸所在的一小段程式,其他部分的修改幾乎都無關痛癢。
但凡事總有例外,有時重構會嚴重影效能,就算如此,我也會選擇繼續重構下去,因為調整重構過的程式容易許多。若重構導致嚴重的效能問題,可在重構後再花時間調整,必要時可以撤銷一些之前做的重構。但在多數情況下,因為重構的關係,可以更有效率地調整效能,最終得到既清楚且快速的程式碼。
對於重構導致的效能問題,整體建議是:在多數情況下,都請無視它。如果重構造成效能變差,請先完成重構,再動手調整提升效能。

看待重構與效能衝突,先重構讓程式好改,之後可再依據效能需求調整,撤銷先前的重構動作也無妨。我想起關聯式資料庫也有「先正規化,再依據效能需求適度反正規化」的設計思維,也是同樣的精神。

想通這點有豁然開朗的感覺,就能繼續看書寫筆記了。老狗也要學會用新角度看世界,加油!

Some thoughts about refatoring and performance issue when reading Martin Fowler's Refactoring


Comments

# by Huang

遠古時代磁碟空間小、RAM小因此都要短小精幹 現在隨便都光速大容易,好讀好寫好維護反而才是王道 不過該精簡的還是要精簡,不能老是當低路師。 電腦不慢,慢的只有大腦而已。 (Win2016、Win10除外)

# by CC

Low Coupling, High Cohesion

# by ByTIM

同個迴圈的變數越多,越容易出錯混淆,把各變數獨立自己一個迴圈,雖然程式碼變長,但對某個數值要異動計算規則時,會方便修改,不會改到其他的變數規則。 補充:我也學到一課了!

# by JasonLo

組織內要避免留下技術債,易懂好維護確實是重點,尤其技術不斷翻新的情況下,真的要一直學會用新的眼光看世界,同為老狗,看你的文章特別有共鳴,感謝你不斷的分享。

# by Lauyea

最近在看Uncle Bob用C#示範有關物件導向原則與設計模式的書,有時候也會看到重構得太「乾淨」的程式碼,覺得不太舒服。 在他本人的Blog中也提到這個問題。 https://blog.cleancoder.com/uncle-bob/2018/08/13/TooClean.html 才發現大師的想法也是會隨時間改變的,他也開始覺得把程式碼寫得太過邏輯分明,反而會讓人難以去修改。 Clean code有時反而會提高複雜度,或只是把髒東西掃到其他地方去而已。 https://programmingisterrible.com/post/173883533613/code-to-debug 也許黑大說的,太過注重效能的程式碼,也是差不多的邏輯吧。

# by sueboy

這個例子有深刻的體認,最近把一個function內,一個超複雜程式內有一個for裡面也是一堆處理,最後也切割開來的,分兩次處理,把問題簡單化,第一次做for 和第二次做for 的差異分離出來,改完後,看程式 和 註解都變的很容易理解,這個核心function到底在幹嘛

# by Allen

不知道這本書翻譯得好不好,我之前買More Effective C#第二版覺得翻譯的超爛,甚至有點像是用Google翻譯翻的.....

# by 艾力萊茵

感謝分享,這篇的確激盪思潮~~

# by Lik

我也是喜歡一個for loop內做幾件事情的人。如果logic不同,的確可以分開。 但是第一種:原本呼叫 playFor(perf) 將結果存入 play 重複使用三次,重構成每次都直接呼叫 playFor() 函式 我是未能理解。先不說效能,起碼打字就多打了些,就算是有智能提示。。。

# by dougpuob

有些不懂其中提到的「故將函式存成變數反覆使用可提升效能」。是指使用 function pointer 還是 getter/setter function ? 好奇那個想法讓大大產生的心魔

# by Jeffrey

to dougpuob, 原文字不夠精確,我做了調整:故將函式**執行結果**存成變數重複利用可提升效能,這樣應該很容易理解了,謝謝提醒。

# by

其實我從微軟推.NET Frmaework時就發現一件事情,其實微軟那個主張是沒錯,但其實要等硬體發展到一個程度時,所謂的"效率"才會有一個突破點,.NET Framework的主張才會被認為是方便開發,在APP一開始流行時也是跟著這個方向走,很多跟效率有關的會因為硬體本身的強大而被弱化,但也不是因為這樣就不能注意效率問題,很多直譯的語言以前常被說效率差,但時間發展久了,硬體、軟體發展愈來愈進步,這問題就會被解決差不多,除非在很受限的環境,像開發Firmware之類的才要注意。

# by PY

OH 我也不是太懂第一個的用意是什麼?

# by 路過俠

個人淺見 在不影響時時間複雜度的情況下 重構通常不用太在意效能差異

# by JohnnyR

我也有心魔了 ... 第一個例子我還是想不懂為什麼要這樣做 如果一個函式在傳入相同參數時每次都會回傳一樣的結果, 為什麼要分三次或每次都去呼叫那個函式重新運算一次?宣告一個變數儲存結果來使用會造成的問題是什麼? 打個比方如果那個函式執行時需從資料庫取得資料,那分多次呼叫就徒增了連線時間。 且以可讀性來說,當我看到宣告一個變數儲存了函式結果,我後面不太需要擔心這個結果發生改變 但多次呼叫函式,可能就會想先確認是不是每次都會回傳相同的結果 如果有人看懂選擇反複呼叫函式的理由可以分享一下嗎?

# by Jeffrey

to JohnnyR,我的理解是使用變數是會讓三次呼叫都依賴那個結果變數,無法基於重構考量把三次邏輯移去更適合的地方(抽取成函式、包裝成物件... 之類)

# by JohnnyR

謝謝黑大回答, 可能我目前的程度還是無法從書中舉的這個例子去理解如此重構的好處及理由, 希望未來有一天會懂 XD

Post a comment