LoginSignup
23

More than 3 years have passed since last update.

Goの構造体初期化はFunctional OptionsパターンとBuilderパターンどっちが良さそうか比べた

Last updated at Posted at 2019-12-06

はじめに

Goのプロジェクトで任意な設定フィールドが多くある構造体の初期化ではFunctional Options パターンがよく使われます。

Functional Options パターンとは?

Functional Options パターンは不透明なOption型を使って内部の構造体に情報を渡すパターンです。 可変長引数を受け取り、それらを順に内部のオプションに渡します。コンストラクタや、公開されたAPIで3つ以上の多くの引数が必要な場合、このパターンを使うと良いでしょう。 (https://github.com/knsh14/uber-style-guide-ja/blob/master/guide.md#functional-options より)

構造体の初期化としてはBuilderパターンも一般的に有名でもちろんGo言語でも実装できます。
特にこの2つのデザインパターンに対して同時に言及した記事がなかったので使い勝手とパフォーマンスの観点で比較してみました。

Goのバージョン
go version go1.13.1 darwin/amd64

TL;DR

比較検証した結果、少々乱暴に結論づけると以下になります。

総合 Functional Optionsパターンの勝ち
使い勝手 Functional Optionsパターンの勝ち
パフォーマンス Builderパターンの勝ち

Main Content

サンプルコードの構成

サンプルコードを作りました。構成は以下のような感じ。

$ tree
.
├── application.go
├── builder.go
├── functional_option.go
└── main.go

ソースコードのリンクはこちら

共通部分

Functional Optionsパターン(以降FOP)とBuilderパターン(以降BP)の比較にて共通で初期化する構造体が以下のApplicationです。

application.go
package main

type Course int

const (
    Basic Course = iota
    Premium
)

func (c Course) String() string {
    switch c {
    case Basic:
        return "Basic"
    case Premium:
        return "Premium"
    default:
        return ""
    }
}

// Application は通信キャリアの契約申し込みをイメージしたものです。
type Application struct {
    Course Course                // ベーシックプランとプレミアプランがある
    SubscribeSupportService bool // サポートサービスのオプション
    SubscribeMovieService bool   // 動画サービスのオプション
    SubscribeBackupService bool  // データバックアップサービスのオプション
}

Functional Options パターン

FOPでApplication構造体を初期化する実装は以下のような感じになります。

functional_option.go
package main

type Option func(*Application)

func WithSupport(flg bool) Option {
    return func(a *Application) {
        a.SubscribeSupportService = flg
    }
}

func WithMovie(flg bool) Option {
    return func(a *Application) {
        a.SubscribeMovieService = flg
    }
}

func WithBackupService(flg bool) Option {
    return func(a *Application) {
        a.SubscribeBackupService = flg
    }
}

func NewApplicationWithFOP(course Course, ops ...Option) *Application {
    a := Application{Course: course}
    for _, option := range ops {
        option(&a)
    }
    return &a
}

Builder パターン

BPでApplication構造体を初期化すると以下のような感じ。

builder.go
package main

type ApplicationBuilder interface {
    WithSupport(flg bool) ApplicationBuilder
    WithMovie(flg bool) ApplicationBuilder
    WithBackupService(flg bool) ApplicationBuilder
    Build() *Application
}

type appBuilder struct {
    course Course
    subSupport bool
    subMovie bool
    subBackup bool
}

func (b *appBuilder) WithSupport(flg bool) ApplicationBuilder {
    b.subSupport = flg
    return b
}

func (b *appBuilder) WithMovie(flg bool) ApplicationBuilder {
    b.subMovie = flg
    return b
}

func (b *appBuilder) WithBackupService(flg bool) ApplicationBuilder {
    b.subBackup = flg
    return b
}

func (b *appBuilder) Build() *Application {
    return &Application{
        Course: b.course,
        SubscribeSupportService: b.subSupport,
        SubscribeMovieService: b.subMovie,
        SubscribeBackupService: b.subBackup,
    }
}

func NewApplicationWithBP(course Course) ApplicationBuilder {
    return &appBuilder{
        course: course,
    }
}

main.go

main.go
package main

import (
    "fmt"
)

func main() {
    // Functional Option Pattern (FOP)
    fopApp := NewApplicationWithFOP(Premium,
        WithBackupService(true),
        WithSupport(true),
        WithMovie(false),
    )

    // Builder Pattern (BP)
    bpApp := NewApplicationWithBP(Premium).
        WithBackupService(true).
        WithSupport(true).
        WithMovie(false).
        Build()

    fmt.Printf("%+v\n", fopApp) // &{Course:Premium SubscribeSupportService:true SubscribeMovieService:false SubscribeBackupService:true}
    fmt.Printf("%+v\n", bpApp)  // &{Course:Premium SubscribeSupportService:true SubscribeMovieService:false SubscribeBackupService:true}
}

使い勝手について

上記のmain.goの見た目で比較してわかるようにこちらの記事などで考慮されている柔軟性に関してはどちらも同じ性質を持っていそうです。
functional_option.gobuilder.goを比較するとBPの方がBuidlerのインターフェースと構造体を用意する必要があり少しFOPよりも複雑になってしまっています。

パフォーマンスについて

ベンチマークをとってみます。

bench_test.go
package main

import (
    "testing"
)

// Functional Options Pattern (FOP)
func BenchmarkFunctionalOption(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = NewApplicationWithFOP(Premium,
            WithBackupService(true),
            WithSupport(true),
            WithMovie(false),
        )
    }
}

// Builder Pattern (BP)
func BenchmarkBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = NewApplicationWithBP(Premium).
            WithBackupService(true).
            WithSupport(true).
            WithMovie(false).
            Build()
    }
}
$ go test -bench . -benchmem
BenchmarkFunctionalOption-4     12982681                85.4 ns/op            64 B/op          4 allocs/op
BenchmarkBuilder-4              25243935                44.8 ns/op            32 B/op          2 allocs/op

上記のコードではBPの方がFOPよりも約2倍早く、メモリアロケーションの数も半分であることがわかりました。

アロケーションされてしまうタイミングをGODEBUG=allocfreetrace=1で見てみ(※1)ると、FOPではWithXXXの関数実行の度にアロケーションが発生していました。Option型の値を返すためにアロケーションが必要なようです。
BPではWithXXXの関数はインスタンス化された構造体の中身を更新するだけなのでアロケーションはなく、New..()Build()の2回のみアロケーションがされていることを確認しました。

この検証でFOPの弱点を見つけることができました。

(※1) 以下のように実行するとトレース結果が出力されます

$ go build -o a.out; GODEBUG=allocfreetrace=1 ./a.out -test.run=none -test.benchtime=1ms 2>trace.log

結論としては

2つのデザインパターンで利用が想定される構造体の初期化は、プロセスとして始めの1度だけ実行されるようなユースケースという前提であれば使い勝手(メンテしやすさ)が優先されるという理由から、Functional Optionsパターンを使っていけば良さそうです。

参考

FOP

BP

Benchmark / Runtime debugging

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23