Go言語のFunctional Options パターンについて
Functional Options パターンとは
■概要
Functional OptionsパターンとはAPIをクリーンに実装することができる実装パターンの一つです。オブジェクトの初期化時にオプションを柔軟・簡潔行うことができる方法として、Goコミュニティで広く採用されています。
■背景
このパターンはRob Pike氏によるSelf referential functions and design というブログ記事にて2014年1月に発表されました。その後2014年10月にDave Cheney氏による Functional options for friendly APIs という記事によって、より具体的に解説されています。
具体例
今回はサンドイッチを例にFunctional Optionsパターンを見ていきます。Sandwich構造体は以下のような構成で、サンドイッチを構成するパンのタイプ・中身・トッピングを指定することができます。
type Sandwich struct {
Bread BreadType
Filling FillingType
Toppings []ToppingType
}
それぞれの構造体には以下のような値が入ります。
BreadType(パンの種類):「ハニーオーツ」・「セサミ」・「ホワイト」
サブウェイのパンの種類を参考にしました😅
FillingType(サンドイッチの中身):「ハム」・「チキン」・「ビーフ」・「ツナ」
ToppingType(トッピング): 「レタス」・「チーズ」・「玉ねぎ」
package main
import "fmt"
type BreadType string
type FillingType string
type ToppingType string
const (
HoneyOats BreadType = "Honey Oats"
Sesame BreadType = "Sesame"
White BreadType = "White"
Ham FillingType = "Ham"
Chicken FillingType = "Chicken"
Beef FillingType = "Beef"
Tuna FillingType = "Tuna"
Lettuce ToppingType = "Lettuce"
Cheese ToppingType = "Cheese"
Onion ToppingType = "Onion"
)
それぞれのオプションを関数として追加できるように定義していきます。
type SandwichOption func(*Sandwich)
func WithBread(bread BreadType) SandwichOption {
return func(s *Sandwich) {
s.Bread = bread
}
}
func WithFilling(filling FillingType) SandwichOption {
return func(s *Sandwich) {
s.Filling = filling
}
}
func WithToppings(toppings ...ToppingType) SandwichOption {
return func(s *Sandwich) {
s.Toppings = append(s.Toppings, toppings...)
}
}
func NewSandwich(opts ...SandwichOption) *Sandwich {
s := &Sandwich{}
for _, opt := range opts {
opt(s)
}
return s
}
このように定義をしたらあとはNewSandwitch関数に必要な設定を関数として渡せば完成です。
func main() {
sandwich := NewSandwich(
WithBread(HoneyOats),
WithFilling(Chicken),
WithToppings(Lettuce, Cheese, Onion),
)
fmt.Println(sandwich)
}
利点
Functional Optionsは複雑なオプションの組み合わせが複数必要な際に力を発揮します。例えばツナサンドイッチ・スペシャルサンドイッチ・チキントマトサンドイッチといったデータを作りたいとしましょう。
■Functional Optionsを使わないパターン
Functional Optionsを使わない場合は以下のようにSandwich構造体をNewする関数を作成します。非常にシンプルですが、もっとオプションが多様化・複雑化した場合に柔軟性を持って対応がしづらいです。
func NewTunaSandwich() *Sandwich {
return &Sandwich{
Bread: WheatBread,
Filling: Tuna,
Toppings: []ToppingType{
Lettuce, Tomato, Cheese,
},
}
}
func NewSpecialSandwich() *Sandwich {
return &Sandwich{
Bread: RyeBread,
Filling: Ham,
Toppings: []ToppingType{
Lettuce, Tomato, Cheese, Pickles,
},
}
}
func NewChickenTomatoSandwich() *Sandwich {
return &Sandwich{
Bread: WhiteBread,
Filling: Chicken,
Toppings: []ToppingType{
Lettuce, Tomato, Mustard,
},
}
}
■Functional Optionsを使うパターン
一方Functional Optionsを使うパターンでは、すでに定義したオプション関数を組み合わせて以下のように実現できます。今後オプションの数や種類が増えても関数を増やさずにすみます。
func main() {
tunaSandwich := NewSandwich(
WithBread(WheatBread),
WithFilling(Tuna),
WithToppings(Lettuce, Tomato, Cheese),
)
specialSandwich := NewSandwich(
WithBread(RyeBread),
WithFilling(Ham),
WithToppings(Lettuce, Tomato, Cheese, Pickles),
)
chickenTomatoSandwich := NewSandwich(
WithBread(WhiteBread),
WithFilling(Chicken),
WithToppings(Lettuce, Tomato, Mustard),
)
}
終わりに
ここまで具体例を使って紹介してきましたが、Dave Cheney氏のブログにはこのパターンを使うことによって以下のような特徴を持ったAPIを構成できると記述されています。