iThome 網站首載:易讀的參數設計
在程式設計中,參數(Parameter)是一種變數,參考至函式或方法等子程式(Subroutine)執行時必要的值,這個值又稱為引數(Argument)。大部份情況下,開發者設計參數時僅關注函式中如何使用參數,忽略了客戶端的易用性,因而使得客戶端在呼叫或閱讀函式時,對於引數的提供感到不便、困惑甚至誤解。參數並非只是函式與客戶端的資料參考,設計參數時可退一步為客戶端著想,讓參數成為函式與客戶端間「溝通」的橋樑。
- 謹慎增加參數的個數
最理想的函式是單參數函式,這樣客戶端就不需要煩惱如何提供引數;實際上,客戶端經常需要提供引數以查詢相關結果,使用引數來做資料轉換,或是產生與引數對應的副作用,然而在考慮增加參數的個數時,應多思考客戶端在呼叫或閱讀時,是否能理解參數的意義。
增加參數個數時應避免的考量之一是要求客戶使用旗標(Flag)。以單參數函式為例,客戶端提供true時做一件事,提供
false
時又做另一件事,當開發者處於函式實作中,雖可從流程中清楚瞭解旗標變數的意義,然而客戶端對於refresh(true)
這樣的程式碼,可能看不出true
的意涵,改為asyncRefresh()
與syncRefresh()
兩個函式,會是比較清楚的作法。旗標變數不見得是布林型態,以JSP中的JspFragment
物件為例,呼叫invoke
時可傳入Writer
實例或null
,代表使用指定的Writer
實例,或是內部堆疊頂端的Writer
實例來進行輸出,這實際上也是個旗標變數,invoke(null)
這樣的程式碼,也容易令客戶端感到困惑。如果函式中完全根據參數值來執行不同流程,也許拆開為數個不同函式會比較好,像是使用
setValue(HEIGHT, 100)
設定高度,setValue(WIDTH, 150)
時設定寬度,不如改為setHeight(100)
、setWidth(150)
來得清楚。直接將函式結果傳給另一函式作為引數,或許也非必要,在《Refactoring: Improving the Design of Existing Code》書中10.8提到的例子是,像discountedPrice(basePrice, getDiscountLevel())
這樣的程式碼,可改為直接在discountedPrice中呼叫getDiscountLevel()
,客戶端使用discountedPrice(basePrice)
會比較清楚。如果函式使用超過一個以上的參數,而參數之間有關聯性,則可建立參數物件(Parameter Object)來包裝它們,這樣參數個數就可減少。例如設定外框時,使用
setOuterBounds(x, y, width, height)
,不如使用setOuterBounds(bounds)
來得清楚,好處不僅是減少了參數個數,在《Implementation Patterns》書中,Kent Beck還談到「很多功能強大的物件,都是從參數物件開始逐漸成長起來」,像是需要將外框擴大時,就可使用setOuterBounds(bounds.expand(-2))
的程式碼讓意圖更清晰。- 適當安排參數的順序
如果參數非得超過一個以上,那麼參數的順序就至關重要,順序基本上以不違反自然應有的組合為原則,像是在指定範圍時,begin會是在end之前,lower會是在higher之前。如果參數間沒有自然的順序,那就參考現有慣例或制訂慣例,透過訓練來熟悉順序,以有序清單為例,將物件加入清單中指定索引處時,第一個參數通常是索引,第二個參數是被加入的物件;在《Clean Code》書中Bob大叔舉
assertEquals(expected, actual)
為例,這也是得經過一段時間的使用熟悉,才不至於搞錯兩個參數的順序。有的函式會有必要參數與可選(Optional)參數,後者通常會在沒有使用者沒有指定引數時提供預設值,常見慣例是必要參數在可選參數之前,有的語言在定義參數時可指定預設引數(Default argument),最好也遵守此慣例,嚴謹的Python會在必要參數定義於預設引數之後時產生錯誤,然而自由度高的Ruby允許這麼定義,這是因為Ruby有它自身的慣例,使用者指定的引數會優先分配給必要參數,多餘引數才依序分配給有預設引數之參數。
對於Java這類不支援預設引數的語言,可使用重載(Overload)來彌補,慣例上具備最少參數的重載版本,上頭的參數就是必要參數,而其他的重載版本,前面的參數要與具備最少參數的版本應當相同。有些語言在呼叫函式時可使用關鍵字引數(Key argument),例如Python可在呼叫函式時以
set_point(y = 10, x = 10, z = 20)
的方式來呼叫,此功能建議用來更直覺地表達可選參數,而非使用於必要參數的指定。有些語言不支援預設引數,然而有適當的資料結構,可用來定義可選物件(Optional object),以JavaScript為例,可以傳給函式
{x: 10, y: 20}
這類的物件,既然稱為可選物件,就代表該物件提供的特性都是可選的值,函式內部會在某特性從缺時提供預設值。不要將參數物件與可選物件混為一談,參數物件中的參數都是必要的,必要參數不應混在可選物件中,例如draw_circle(radius, {x: 10, y: 20})
,表示radius
是必須的,而x
、y
會是可選的。有的語言支援可變長度引數,雖然從客戶端來看,可以提供函式任意數量的引數,然而基本上函式就是使用單一參數來接受引數清單,使用上建議對清單中的引數平等看待,不要有特定順序問題。接受可變長度引數的參數,應該在必要參數與具備預設引數的參數(可選參數)之後。
- 避免不必要的輸出用引數
在提供引數給函式時,通常是作為函式執行時的輸入,如果函式會產生結果,通常是預期以傳回值方式取得。不過有時可見到,函式會改變輸入的引數狀態,像是Python的
sort
函式,會直接改變傳入的list物件為排序狀態,一般來說不鼓勵輸出用的引數,因為違反呼叫時引數是作為函式輸入的預期,如果不希望使用輸出用引數,可像Python使用sorted
函式來傳回一個新的list物件,作為排序後的結果。一個不當使用輸出用引數的情況是,為了突破函式傳回值只能是單一值或型態的限制,想在一次函式呼叫中取得多個結果,像是希望在函式呼叫後,想要同時取得大於及小於某數的兩個數列,因而傳入了兩個list作為輸出用的引數,這當中必須檢討的是,該函式是否做了兩件以上的事,是否可以拆開為兩個函式分別負責,如果真的想要傳回兩個以上的值或型態,可以使用元組(Tuple)或自定義物件來包裝,而不是使用輸出用引數。
不過在《Implementation Patterns》中舉了個收集參數(Collecting Parameter)的例子,Kent Beck提到「有時計算邏輯需要從多次的方法呼叫中收集結果,並將這些結果以某種方式合併起來」,若合併方式有一定的複雜度,那麼使用一個參數來收集結果就較為直覺了,像是在JUnit中,使用
TestResult
來收集測試結果就是實際案例,因為它必須在多個Test
實例的runTest
方法中收集測試結果。- 考慮讓引數來自物件狀態
如果必須得改變物件狀態,像
sort
這類的函式,似乎很難避免輸出用引數的方式,如果程式採用物件導向典範,可將這類函式定義為物件本身的方法,像是Python中使用[1, 2, 3, 4].sort()
,就可避免使用輸出用引數,因為物件本身(self
,其他語言中可能是this
)狀態就取代了輸出用引數。實際上,在《Refactoring: Improving the Design of Existing Code》書中,雖然有時沒有明確指出,但在考慮到物件狀態時,往往可以減少參數的使用,該書討論第一章的案例時,發現
Customer
類別的amountFor
方法有個接受Rental
實例的參數,然而卻沒有使用來自Customer
的資訊或方法,因而將amountFor
方法重構至Rental
類別,因而就不再需要任何參數。在討論API設計時,開發者往往僅著重設計模式、架構與各種設計原則的實現,以讓程式更有彈性且可維護,卻較少考慮到參數設計,實際在身份轉為呼叫函式的客戶端時,不良的參數設計經常發生困擾,在專注於函式實作之後,應該經常轉換為客戶端角度,檢視一下參數設計是否造成客戶端與函式間的溝通不良,或者是使用上的不便。