Functional Options パターンによる Go の柔軟な構築パターン

Go 言語には関数のデフォルト引数が存在しないため、柔軟かつ拡張性のあるコンストラクタを実現するために Functional Options パターン が広く使われています。このパターンでは、変更可能な構成オブジェクトに対して、オプションとして関数(キャンセラブルな構成ステップ)を適用し、最終的なインスタンスを生成します。

たとえば、PC の構成を自由に指定できるコンポーネントを持つオブジェクトを設計するとします。

type PC struct {
    Name       string
    CPU        string
    GPU        string
    MemoryGB   int
}

まずは、内部で利用する構成情報をющий型を定義します。この型は初期状態として、最低限の仕様を持ち、後続のオプション関数によって上書きされます。

type buildConfig struct {
    cpu        string
    gpu        string
    memoryGB   int
}

var defaultConfig = buildConfig{
    cpu:        "Ryzen 3 3200G",
    gpu:        "Integrated",
    memoryGB:   8,
}

次に、構成を変更するための関数ファクトリを定義します。これらはクロージャとして実装され、受け取った構成オブジェクトを直接変更します。

type Option func(*buildConfig)

func WithCPU(model string) Option {
    return func(cfg *buildConfig) {
        cfg.cpu = model
    }
}

func WithGPU(model string) Option {
    return func(cfg *buildConfig) {
        cfg.gpu = model
    }
}

func WithMemory(sizeGB int) Option {
    return func(cfg *buildConfig) {
        cfg.memoryGB = sizeGB
    }
}

構築関数 itself は可変長引数として Option 関数を受け取り、デフォルト構成を基にそれらを順に適用して最終状態を確定させます。これにより、引数の並び順や省略が自由に可能になります。

func BuildPC(name string, opts ...Option) *PC {
    cfg := defaultConfig
    for _, opt := range opts {
        opt(&cfg)
    }
    return &PC{
        Name:     name,
        CPU:      cfg.cpu,
        GPU:      cfg.gpu,
        MemoryGB: cfg.memoryGB,
    }
}

実際に利用する場合は、以下のように記述できます。

func main() {
    // デフォルト設定で作成
    pc := BuildPC("workstation")
    
    // 特定オプションのみ上書き
   高性能PC := BuildPC("gaming-pc",
        WithCPU("Ryzen 9 5900X"),
        WithGPU("RTX 3080"),
        WithMemory(64),
    )
}

このパターンの利点として、既存の API を拡張する際にもブランド新しいオプションを追加するだけで済み、破壊的変更にならない点が挙げられます。たとえば、CPU クラスタに対応する WithCluster など、新たな引数を容易に追加可能です。

タグ: functional-options go-pattern variadic-parameters constructor-pattern

6月16日 21:15 投稿