前言
之所以使用 Functional options 的契機,是因為用到 gRPC 的 New Server API,發現他是用 functional options 來讓使用者調整 Server 預設配置,這樣的作法不但兼具了擴充性和可用性,也能避免一些使用者誤用。而除了看 source code 來學習如何實作之外,也找起相關文章,進而發現原來早在 2014 年就有人發表過類似教學文,實在是太孤陋寡聞了~

趁著這次機會,把相關文章的重點整理出來,讓大家在寫類似 API 時,也能做個參考。
Self-Referential Functions
首先要提到的是由 Rob Pike 所整理出 self-referential functions 的文章,此 Pattern 方式可用於:
- 有效地處理繁多且複雜的 setting options
- 需要保留之前所設定的 option
以下為文章中的實作例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// 1. 定義 Option Type. Foo 為一個 struct 內含我們需要調整的變數
type option func(f *Foo) option
// 2. 使用 closure 來定義一個可以調整特定變數的 function
func Verbosity(v int) option {
return func(f *Foo) option {
// 取出之前設定值
previous := f.verbosity
f.verbosity = v
// 回傳包含之前設定值的 option
return Verbosity(previous)
}
}
// 3. 接下來建立一個 function 來套用這些 options
func (f *Foo) Option(opts ...option) (previous option) {
for _, opt := range opts {
previous = opt(f)
}
return previous
}
// 4. Usage
prevVerbosity := foo.Option(Verbosity(3))
foo.DoSomeDebugging()
foo.Option(prevVerbosity)
|
從例子中可以看到,此 Pattern 利用 Closure 特性來生成 option type ,透過這樣的方式,可以簡單地套用這些 options 來達到修改指定變數之目的,且也能回復到之前設定的值,當然如果你不想 return 先前的 option 也行。總而言之,這樣的設計可以使用於多數需要進行參數配置的場景。
Functional options for friendly APIs
而 Dave Cheney 則是在這文章中用 functional options 來說明類似概念,不過文章中有舉例出過往為了解決這類 setting options 所用的各種方法,並點出這些方法的優缺點和強調 Functional options 的優勢,好讓使用者能做個比較。
方法:
- 在 funcation 中引入所有可以調整的參數
1
| func NewServer(port int, addr string)
|
這樣的寫法很直覺,但是一旦需要很多項設定時就很難擴充。
- configuration struct
1
2
3
4
5
6
7
8
| type Configuration struct {
port int
addr string
}
func NewServer(config Configuration) {
//
}
|
使用 configuration 應該是蠻常見的作法,這樣提高的擴充性,不過卻也增加一些風險,例如套用 default 行為時,需要判斷這些變數是否存在再套用。當然你也可以設定一個 function 然後 return default configuration 來讓使用者套用。
1
2
3
4
| NewServer(Configuration{}) // May cause error
defaultConfig := default()
NewServer(defaultConfig)
|
不過,還有沒有其他的作法呢?
Functional Options
Functional Options 和上面所提及的 Self-Referential Functions 作法相似,一樣是藉由多個 functions 來修改特定變數值。而為了套用 Default 行為,我們可以事先定義一個含有預設值的變數,然後再根據使用者所引入的 options 來修改此預設變數。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 1. create a configuration type
type Configuration struct {
port int
addr string
}
// 2. Create a default config
var defaultConfig = {
port: 8080,
addr: "localhost"
}
// Create a option type and some closure functions to return option
// Same as `Self-Referential Functions`
// 3. Use options with default config
func NewServer(opts... Option) {
// use these option functions to modify your default configuration
for _, opt := range opts {
opt(&defaultConfig)
}
}
|
以上的 code 也是目前 gRPC 所使用的配置方式 - gRPC NewServer function source code
1
2
| // gRPC Example
s := grpc.NewServer(grpc.StatsHandler(&ocgrpc.ServerHandler{}))
|
References
- Self-referential functions and the design of options
- Functional options for friendly APIs