見出し画像

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を構成できると記述されています。

sensible defaults(賢明なデフォルト設定)
is highly configurable(高度な構成可能)
can grow over time(時間をかけてAPIを成長させられる)
self documenting(自己文書化している)
safe for newcomers(初めてAPIを触る人に優しい)
and never requires nil or an empty value to keep the compiler happy(コンパイラを通すために必要のないフィールドにnilや空白文字を入れる必要がない)

Functional options for friendly APIs

出典