0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goの豆知識 -- Optionパターン

Posted at

1. はじめに

Go 言語は 1.0 の頃から関数を 第一級オブジェクト(first-class
citizen)

として扱うことができ、さらにクロージャもサポートしています。
そのため、Option パターンは特定のバージョン機能に依存せず、Go の基本機能だけで実現できます。

2014 年、Go コミュニティで有名な Dave Cheney 氏がブログ
「Functional Options for Friendly APIs」
にて初めて体系的に提案し、その後 Go
コミュニティの慣用的な書き方として広まりました。


2. 背景

従来の Go のコンストラクタでは次のような課題があります:

  • Go には デフォルト引数関数オーバーロード が存在しない
  • 引数が 3〜4 個を超えると、呼び出し側の可読性が急激に低下する
  • 新しい引数を追加する際に、新しいコンストラクタを追加せざるを得ず、API
    が肥大化する
  • デフォルト値を優雅に扱えない
  • 課題:コンストラクタの引数が増えるほど可読性・保守性が悪化する
  • 解決策:関数型プログラミングの発想を応用し、引数を「オプションの関数片」にカプセル化する
type Server struct {
    Host string
    Port int
    Timeout int
    Password string
    Option string
}

func NewServer(host string, port int, timeout int, password string, option string) *Server {
    return &Server{Host: host, Port: port, Timeout: timeout, Password: password, Option: option}
}

// 引数が長すぎてミスしやすい
serv := NewServer("0.0.0.0", 8080, 30, "111111", "option")

new で Server
を作ると長すぎてミスしやすいし、読みづらい。もっと簡潔で分かりやすく書けないでしょうか?
→ はい、それを解決するのが Option パターン です。


3. 使い方

Option 型(関数型)の定義

type Option func(*Server)

type Server struct {
    Host string
    Port int
    Timeout int
}

SetXxx に相当するクロージャ関数を提供

func WithHost(host string) Option {
    return func(s *Server) {
        s.Host = host
    }
}

func WithPort(port int) Option {
    return func(s *Server) {
        s.Port = port
    }
}

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.Timeout = timeout
    }
}

プリセットの「デフォルト値」も簡単に定義できます:

func WithTimeoutTypeA() Option {
    return func(s *Server) {
        s.Timeout = 30
    }
}

s := NewServer(
    WithTimeoutTypeA(), // デフォルト値を適用
    WithTimeout(60),    // 上書き
)

⚠️ Option は適用順に上書きされるため、指定の順序に注意が必要です。

コンストラクタで Option を適用

func NewServer(opts ...Option) *Server {
    s := &Server{
        Host: "127.0.0.1",
        Port: 8080,
        Timeout: 30,
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

使用例

// デフォルト設定
s1 := NewServer()

// カスタム設定
s2 := NewServer(
    WithHost("0.0.0.0"),
    WithPort(9090),
    WithTimeout(60),
)

4. 解決できる問題

従来の書き方 → Option パターンの比較

// 従来:必ず全引数を渡す必要あり
s := NewServer("127.0.0.1", 8080, 30)

// Option パターン:引数に意味が付与される
s := NewServer(
    WithHost("127.0.0.1"),
    WithTimeout(45),
)
  • コードの意味が一目で分かる
  • 新しいパラメータを追加しても WithXxx
    を増やすだけで既存の呼び出しは壊れない
  • デフォルト値がコンストラクタに内蔵され、呼び出し側は省略できる

5. 有名プロジェクトでの採用例

gRPC-Go クライアント

gRPC-Go
のソースコード

では DialOption が定義されています:

type dialOptions struct {
    block           bool
    returnLastError bool
    timeout         time.Duration
    ...
}

// インターフェース型 Option の定義
type DialOption interface {
    apply(*dialOptions)
}

// ファクトリーラッパ
type funcDialOption struct {
    f func(*dialOptions)
}

func (fdo *funcDialOption) apply(do *dialOptions) {
    fdo.f(do)
}

func newFuncDialOption(f func(*dialOptions)) *funcDialOption {
    return &funcDialOption{f: f}
}

// 公開されている関数
func WithInsecure() DialOption {
    return newFuncDialOption(func(o *dialOptions) {
        o.insecure = true
    })
}

func WithBlock() DialOption {
    return newFuncDialOption(func(o *dialOptions) {
        o.block = true
    })
}

使用例:

conn, err := grpc.Dial("localhost:40001",
    grpc.WithInsecure(),
    grpc.WithBlock(),
)
  • grpc.Dial
    には認証、LB、インターセプタなど大量のオプションが存在する
  • Option パターンを インターフェース型
    で実装しており、関数型との違いは定義の仕方だけ。使い方は同じ
  • 大規模ライブラリに適しており、柔軟な拡張性を提供できる

AWS / GCP SDK

AWS SDK for Go でも WithRegionWithCredentials
などのオプション指定が採用されています。


6. まとめ

  • Option パターンは Go コミュニティにおける「ベストプラクティス」
  • 本質は 関数型プログラミングの発想を用いた柔軟な API 設計
  • 解決できる課題:
    • コンストラクタの 引数爆発問題
    • デフォルト値の管理
    • 拡張性の不足
  • デメリット:デバッグ時に Option
    の呼び出しチェーンを追う必要があり、やや複雑になる
  • しかし 可読性・保守性の大幅な向上
    に比べれば、十分受け入れ可能なコス
  • 将来的に Go
    が「名前付き引数」や「デフォルト引数」を導入すれば部分的に置き換わる可能性はあるが、現状では最良の解決策といえる

7. 参考資料

  1. Dave Cheney, Functional Options for Friendly APIs, 2014
    https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis

  2. Dave Cheney, dotGo 2014 - Functional Options for Friendly
    APIs
    (講演動画)
    https://www.youtube.com/watch?v=24lFtGHWxAQ

0
3
0

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
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?