Go言語のFunctional Option Pattern

  • 92
    いいね
  • 0
    コメント

オプション

パッケージを作る際、柔軟性を持たせるためにオプションを持たせたい時がしばしばあります。
しかしオプションは知っての通り設定しないことが少なくありません。
単にコンストラクタに並べるようでは無用な複雑さをはらむことになります。

JavaなどではOptional Parameterなどのように、デフォルト値が指定できる機能があります。
機能の厳選されたgo言語ではそのような機能はありませんが、
"Self Referential Functions Design"というテクニックがあり、
それについての記事がRob Pike氏の記事を筆頭にいくつか説明されています。
オプションと相性が非常に良いため、合わせて"Functional Option Pattern"とも呼ばれています。
Dave Cheney氏の記事を参考におおまかに説明したいと思います。

様々な解決策

あるServerクラスがあり、そこにタイムアウトと接続数の制限の機能を追加したいとします。

    //最もシンプルなやり方
    server.New("localhost",30*time.Second,10)

ある程度作り込むパッケージであればおそらく大半のプログラマがこのやり方は避けると思います。
理由としては常に詳しく設定しなければいけないこと
拡張するたびに引数が増えること
値の意味が不明瞭なことなどとにかくデメリットまみれです。
もちろん引数の拡張の見込みがなく
オプション自体わずかで済むのであればこういった場合の方が良い時もあると思います。

    //解決策?
    server.NewServerWithTimeout("localhost",30*time.Second)
    server.NewServerWithMaxCon("localhost",10)
    server.NewServerWithTimeoutAndMaxCon("localhost",30*time.Second,10)

引数は明示的になり必要なものだけ渡すことが可能になりましたが
拡張の度に指数的に関数が増加するというおぞましい構成になります。
こちらもまず使う人はいないと思います。

    //おそらく一般的な解決策
    server.New("localhost",server.Config{
        Timeout:10*time.Second,
        MaxConnection:10,
    })

おそらく一般的な解決策はこれになると思います。
実際私もこういったやり方を使っていました。
省略可能で、拡張性があり、明示的ではありますが
デフォルト値の扱いが怪しいところがあります。

    //ポートを新たに追加、デフォルトは8080
    server.New("localhost",server.Config{
        Port:0 //8080に設定される
    })
    //一つも指定しなくてもConfigは渡さなければいけない
    server.New("localhost",server.Config{})

サーバーのポートは、指定しなければ8080が割り当てられるというのが一般的なので
ゼロ値であれば8080を設定するようにしたいですが
上記の場合、0と明示的に指定しているのに8080が設定されるという妙な挙動になってしまいます。
また、一つも指定していないにもかかわらずConfig構造体を渡すというのも冗長です。
後者はポインタにすることでnilを渡すことも可能ですが
コンストラクタ使用後にコンフィグデータを弄った場合の挙動が
コンフィグの値を直接参照しているのか、コンストラクタの時点で確定するのかによって変わり、
期待通りの挙動にならない可能性がありますし、結局何かしらを渡すことになります。

解決策"Functional Option Pattern"

ここで本題の解決策です。

option.go
package server

import "time"

type Option func(*Server)error

func Timeout(t int)Option{
    return func(s *Server)error{
        return s.setTimeout(time.Duration(t) * time.Second)
    }
}
func MaxConnection(c int)Option{
    return func(s *Server)error{
        s.maxConnection = c
        return nil
    }
}

上記の形でオプション設定関数を作る関数をコンストラクタに可変長引数として渡すことで

    server.New("localhost",server.Timeout(30),server.MaxConnection(10))

こんな感じになります。
これによって

  1. デフォルト値 (完全に独立したデフォルト値が指定できる)
  2. 設定自由度 (自由な組み合わせが指定できる)
  3. 拡張性 (オプションが増えても関数を増やすだけ)
  4. 自己説明能力 (パラメータの名称が明示されます)
  5. 安全性 (コンフィグをポインタで渡したような挙動の推測しづらい弄り方ができません)
  6. 不要な引数の完全な排除 (設定しない項目はもちろん、全て設定しない場合も隠蔽できます)

すべてが解決されます。すごい。

実装はややまどろっこしくはなりますがパッケージは使う側のことを考えて作るものです。
パッケージのオプションを実装する場合、Functional Option Patternを活用することで
あなたのパッケージが強烈無慈悲なオシャレパッケージになるかもしれません。
試してみてはいかがでしょうか。
あまりにオシャレすぎるため乱用したくなりますがgodocにオプション関数が並ぶので
本当に必要かどうかをよく考えて使いましょう。

ちなみにgo言語のprivateはパッケージ内でのアクセスなので
今回のようなメンバでない関数でもプライベートにアクセスできます。
go言語はこういう細かいところも上手くできていますね。すごい。

参考記事

-Rob Pike
原典です。本記事が誤解だらけであってもとりあえずこれを読めば間違いありません。
https://commandcenter.blogspot.jp/2014/01/self-referential-functions-and-design.html
-Dave Cheney
pkg/errorsパッケージで有名な人です。具体的な例をつかって説明されています
https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis