Go言語のFunctional Option Patternの実装してみた

Go言語で仕事をする可能性があったのでGo言語の記事を読みあさっていたいました。

Functional Option Patternの記事を見てこのパターン使ってみたい!とこみ上げるものがあったのでWorker数などの内部挙動をオプションで制御できるTaskDispatcherを実装することにしました。

テーマとなっているDispatcherの生成時のカスタマイズ以外の内部実装はほとんどkaneshinさんのブログを参考にさせていただきました。

リポジトリはここです。

Functional Option Pattern と interfaceによる抽象化のしっくりこない感じ

Functional Option Patternの紹介の中の例を参考にまず書いてみた処、下記のようになります。

// OptionでDispatcher内部の値を操作する
//
//   d := new(Dispatcher)
//   var opt Option = SomeEffectOption()
//   opt(d)
type Option func(*Dispatcher) error

引数がDispatcher構造体のポインタ型になっています。

ただライブラリの開発ではパッケージ外にexportするものの実装を抽象化するためにinterfaceで実装したい感じもします。

それを踏まえコードに起こすと下記のようになります。


type Option func(Dispatcher) error

// Dispatcherのinterface
type Dispatcher interface {
    Dispatche(t Task) error
    Start() error
    Wait() error
    Stop(force bool) error
}

このまま実装を進めてしまうと、下記のようにSetterを実装するなどのようなことになってしまい、 Dispatcherをinterfaceで実装するメリットがなくなってしまいます。


type Option func(Dispatcher) error

func MaxWorker(n int) Option {
    return func(d Dispatcher) error {
        retrun d.SetMaxWorker(n)
    }
}

type Dispatcher interface {
    // 上記は省略
    SetMaxWorker(n int) error
}

この方法は思ってたFunctional Option Patternと違ったので、 Dispatcherの仕様に関する管理をConfig構造体の責務にしました。

// OptionはConfigに影響を与えるように
type Option func(*Config) error

// Dispatcherの設定を管理する。
type Config struct {
    MaxWorker int
}

// Optionの設定項目の例
func MaxWorker(n int) Option {
    return func(c *Config) error {
        retrun c.MaxWorker = n
    }
}

// dispatcherはDispatcher interfaceを実装するように
var _ Dispatcher = &dispatcher{}

// OpionからのDeipatcherで生成をする
//   d := New(MaxWorker(5), SomeOption("hoge"))
func New(opts ...Option) (Dispatcher, error) {
    c := new(Config)
    // OptionにConfigを適応する
    for _, opt := range opts {
        if err := opt(c); err !=nil {
            return nil, err
        }
    }
    // Configからの生成は任せる
    return CreateFromConfig(*c)
}

// ConfigからDispatcherを生成する
func CreateFromConfig(c Config) (Dispatcher, error)

// Dispatcherの内部実装
type dispatcher struct {
    c Config

    // 説明以外の箇所は省略
}

ユーザーの指定したOptionの管理とそこからのDispatcherの生成はConfig構造体に全て任せるパターンで実装しました。

下記のようにFunctional Option Patternな書き方になります。

package main

import "github.com/kamiazya/go-dispatcher"

func main() {
    // Dispatcherの生成
    d, _ := dispatcher.New(
        dispatcher.MaxWorker(5),
    )

    // 稼働開始
    d.Start()

    // タスクのDispatch
    d.Dispatch(func() {})
    d.Dispatch(func() {})

    d.Stop(false)
}

まとめ

Functional Option Patternだと構造体をinterfaceにして抽象化するときにややこしくなるが、生成までのユーザー設定の管理をConfig構造体に責務分離することでinterfaceの汚染を防ぎながら実装できました。

余談

構造体の名前ですがConfigSpecかで今も迷っています。

ConfigSpecの変更しやすさのイメージを考えたとき、Configのほうが変更しやすそうなので、Configにしました。

参考