在《Clean Code》第三章一開始,Bob大叔就說了:「函式的首要準則,就是要簡短。第二項準則,就是要比第一項的簡短函式還要更簡短。」。

類似的概念,也出現在Martin Flower的《重構》、Kent Beck的《Smalltalk Best Practice Patterns》等大師級著作之中。只是,什麼樣的函式才是簡短?有什麼樣的思考原則?簡短的目的又是什麼呢?

簡短的概念

如果談到一行程式該有多長,許多風格指南都會有個明確數字,通常建議是在80個字上下,儘管這個數字是基於60年代的機器限制而遺留下來,現代的設備有著更寬的顯示範圍,然而,多數開發者均認同此一原則,如果使用整合開發工具,編輯器上通常會有一條垂直線作為提醒,必要的話,通常也允許你改變這條線內可容納的字元數量。

若談到一個函式的程式碼行數,意見就比較分岐了。也許你曾經試著將每個函式的行數控制在20行以內,不過,可能會發現很難做到。畢竟有時還得考慮排版問題,像是為了可讀性,而將選項物件(Option object)作點換行;或許也曾發現某個天才,為了簡短而將每個函式本體縮短到如同天書,難以理解。由此可見得,函式也不全然是越短越好。

Bob大叔談到:「函式不應該大到包括巢狀結構」、「函式裡的縮排程度不應該大過一或兩層」,這似乎是個不錯的準則,往往地,下一個巢狀結構或縮排,無論是視覺或者是實際動作上,都代表著這是另一個層次的任務,「函式應該只做一件事」這樣的建議,在許多談及程式碼維護的書中,應該是屢見不鮮了。

在想要撰寫簡短函式時,「函式應該只做一件事」的原則會比行數、巢狀或縮排來得實際,然而,往往你會停下來思考,若在函式中呼叫了a函式,接著寫些流程處理,之後又呼叫了b函式,而這樣是做了幾件事呢?

《Clean Code》與《Smalltalk Best Practice Patterns》中,有類似的建議:只要這三個動作都是在「同一層抽象概念」,那麼,就算是只做了一件事。只是,「同一層抽象概念」這件事,本身就很難斷定,因此,我們需要一些手段來協助判別。

簡短的手段

實際上,行數、巢狀結構或者是縮排,都是個判別的手段。畢竟一個幾百行甚至上千行的函式,或者是有著像波動拳推擠出來的巢狀縮排(https://goo.gl/OM9FCG),絕對是有著過多的任務與細節。

像《重構》這類的書籍都會談到:面對這樣的函式時,如何將之大卸八塊。基本上,重構是一種自頂而下(Top-down)、將一個大函式實現過程,重新使用小函式來加以詮譯的作法,如果每個小函式負責的任務適當,可讀性與維護性就能大幅提升。

有時會是面對自己寫的程式碼。畢竟有時寫得忘我,或者是迫於某些外在因素,而使得函式逐漸肥大起來,時時回頭審視並實行重構,是對函式去油解膩的方式之一,只是,有沒有方式,可以在一開始就儘量寫出簡短的函式呢?

若要採取比較激烈的手段,強制讓變數不可變是個方式。在純函數式的世界,若覺得某變數的值應該變換,代表著現階段任務完成,可以就計算出來的值,進行下個任務。這個手段之所以比較激烈,是因為這會強制自頂而下地思考有哪些子任務,然而,這些卻必須自底而上(Down-top)地實作。

想想看,為什麼明明很想將函式寫得簡短,卻往往又變得冗長呢?

通常就是定義一個新函式,然後,就邊思考邊寫下函式的內容。因為就在同一個函式中,在實現下一任務的時候,很容易會因為一時方便,而存取了前一任務的情境,使得多個子任務之間的職責混淆不清。

而強制讓變數不可變的話,會發現很難邊思考邊寫,基本上,需經過整體思考,從最基礎的函式開始實現,待基礎函式都完成後,才能往更高層次的函式實現推進。

在能掌握自頂而下地思考,以及自底而上地實作之後,堅持純函數式就並非必要了。

而這有個好處,在撰寫基礎函式的時候,由於沒有更高層的任務情境可以參考,就能專心地思考目前函式該如何完成任務,而不用顧及其他同層次或更高層次的任務。因此,如果目前函式需要迴圈迭代,那就用吧!

若是自頂而下地實現,也許會因此而考慮在一個迴圈迭代中,順便做些其他的事以爭取效能。但這就會讓函式混入了不同的抽象層次,而做了更多件事情,函式自然就會肥大了。

不要畏懼增加子函式

撰寫簡短函式時,容易會有的阻礙是:老想著「這邊又抽出一個子函式,會不會太麻煩了?」。

會覺得麻煩的原因之一,可能是命名。

曾經有些單位做過調查,程式設計師覺得最難的,往往就是命名,例如〈Arg! The 9 hardest things programmers have to do〉(https://goo.gl/XXNfjD)中的第一名,就是Naming things。而且,函式命名最怕的,大概是名稱可能很長,對此,Bob大叔的建議是,如果具描述性質就不用在意,《Clean Code》第二章也談了一些〈有意義的命名〉原則。

另一個麻煩的地方,在於參數。參數的個數、順序、意義等,考慮的彈性越多,參數設計就越麻煩。這也就是為什麼,我之前曾經收集了一些參數設計的原則,將心得寫在〈易讀的參數設計〉(http://www.ithome.com.tw/node/83808)中,以便日後參考。但是,不可否認地,設計參數的麻煩,經常還是會讓我懶得再為某個子任務,而抽取出一個子函式。

有時,我會採取一個折衷的方式。如果語言支持區域函式的話,就用區域函式來組織某個子任務,而該函式可直接「取用」外部函式的區域變數,只有一些必要的值透過參數傳遞,這可減少參數設計的麻煩。

然而這方式有個原則,區域函式與取用的外部區域變數之間,距離不能太遠,而且,在區域函式中,不能改變外部區域變數的值,這樣可以避免區域函式干擾了外部函式負責的任務。

另一個好處是,如果發現有另一個函式中,有著類似的需求,可以輕易地將區域函式抽取出來,成為公用的外部函式。而這只要在函式介面上,增加一個新的參數,來取代原本的外部區域變數。

由於這時有兩個函式會呼叫被抽取出來的子函式,該函式負責的任務就明確多了,命名與參數設計上,也會比較容易,而且,也可以避免過度設計的疑慮,也就是懷疑抽取出來的函式,純粹只有一個函式會呼叫的問題。

如果是物件導向程式設計的話,區域函式可能是先抽取出來成為物件方法。像Java的話,一開始這個方法可能是私有的(private),如果發現子類別或物件之間會需要使用此方法溝通,再進一步考慮開放為受保護(protected)或者是公開(public)方法。

子任務被抽離並獨立解決

談到函式必須要簡短,大家贊同的原因之一,就是能大幅增加可讀性,以及日後修改維護時的方便性,簡短的函式,基本上,是比冗長的函式容易閱讀。不過,如一開始說的,並不是越短越好。因為,簡短是一種手段,目的是為了容易觀察出函式中的任務,是否屬於同一層抽象概念。

無論是採取重構將肥大函式分解,而得到同一層抽象概念,或者自頂而下地思考,自底而上地實作出同一層抽象概念,重點都在於將子任務抽離並獨立地解決,獨立地解決子任務,函式自然簡短,審視函式時若能掌握這首要之務,那麼就不會再去斤斤計較函式的行數、巢狀結構或縮排有幾層了。

作者簡介


Advertisement

更多 iThome相關內容