大部份情況下,開發者設計參數時僅關注函式中如何使用參數,忽略客戶端的易用性,因而使客戶端呼叫或閱讀函式時,對於引數的提供感到不便、困惑甚至誤解。參數並非只是函式與客戶端的資料參考,設計時可退一步為客戶端著想,讓參數成為函式與客戶端「溝通」的橋樑。

謹慎增加參數的個數
最理想的函式是單參數函式,這樣客戶端就不需要煩惱如何提供引數。實際上,客戶端經常需要提供引數以查詢結果,使用引數來轉換資料,或是產生與引數對應的副作用,然而在考慮增加參數的個數時,應多思考客戶端在呼叫或閱讀時是否理解。

增加參數個數時,應避免的考量之一,是要求客戶使用旗標(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》書中,舉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會是可選的。

有的語言支援可變長度引數,雖然從客戶端來看,可以提供函式任意數量的引數,然而,基本上函式就是使用單一參數來接受引數清單,使用上建議對清單中的引數平等看待,不要有特定順序問題。接受可變長度引數的參數,應該在必要參數與具備預設引數的參數(可選參數)之後。

適當安排參數的順序
如果參數非得超過一個,那麼參數順序就至關重要,順序基本上以不違反自然應有的組合為原則,像指定範圍時,begin會在end之前,lower會在higher之前。如果參數間沒有自然的順序,那就參考現有慣例或制訂慣例,透過訓練來熟悉順序。

以有序清單為例,將物件加入清單中指定索引處時,第一個參數通常是索引,第二個參數是被加入的物件;在《Clean Code》書中,舉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設計時,開發者往往僅著重設計模式、架構與各種設計原則的實現,以讓程式更有彈性且可維護,卻較少考慮到參數設計,實際在身分轉為呼叫函式的客戶端時,不良的參數設計經常發生困擾。所以,在專注於函式實作之後,應該經常轉換為客戶端角度,檢視一下參數設計是否造成客戶端與函式間的溝通不良,或者是使用上的不便。

 

作者簡介


Advertisement

更多 iThome相關內容