從事程式開發超過二十年,我一直不是開發方法論的信徒,對於 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 我也不是太懂第一個的用意是什麼?

Post a comment


51 + 20 =