Menu
Coddy logo textTech

Functional Options パターン

CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 79/107。

多くのオプション設定フィールドを持つ構造体を作成する場合、コンストラクタ関数が扱いづらくなることがあります。Functional Options Patternは、関数を使用してオブジェクトを構成することで、この問題をエレガントに解決します。

複数のオプション設定を持つサーバー構造体を考えてみましょう。多くの引数を持つコンストラクタを使用する代わりに、オプション型とオプションを返す関数を定義します。

type Server struct {
    host    string
    port    int
    timeout int
}

type Option func(*Server)

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 NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,    // デフォルト
        timeout: 30,      // デフォルト
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

これで、サーバーの作成が読みやすく、柔軟に行えるようになります:

// デフォルトを使用する
s1 := NewServer("localhost")

// 特定のオプションをカスタマイズする
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))

このパターンが優れているのは、呼び出し側が必要なものだけを指定すればよく、デフォルト値が一箇所で明確に定義されており、新しいオプションを追加しても既存のコードを壊すことがないからです。grpczapといったGoのライブラリで広く使われています。

challenge icon

チャレンジ

簡単

Functional Options パターンを使用して、設定可能なデータベース接続を構築しましょう!このパターンは、適切なデフォルト値を持ちつつ、多くのオプション設定が必要な場合に非常に有効です。まさにデータベース接続に最適な手法です。

コードは 2 つのファイルに分けて構成します:

  • database.go: データベース接続の型とオプション関数を定義します。

    以下の非公開(unexported)フィールドを持つ DBConnection 構造体を作成してください:

    • host (string)
    • port (int)
    • username (string)
    • password (string)
    • maxConnections (int)
    • timeout (int) - 秒単位

    *DBConnection を変更する関数として Option 型を定義します。

    Option を返す以下のオプション関数を作成してください:

    • WithPort(port int)
    • WithCredentials(username, password string)
    • WithMaxConnections(max int)
    • WithTimeout(seconds int)

    *DBConnection を返すコンストラクタ NewDBConnection(host string, opts ...Option) を作成してください。デフォルト値として、port に 5432、username に "admin"、password に "secret"、maxConnections に 10、timeout に 30 を設定します。デフォルト値を設定した後、提供されたすべてのオプションを適用してください。

    以下の形式で接続情報を返す ConnectionString() メソッドを追加してください:

    [username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)
  • main.go: さまざまな設定でデータベース接続を構築します。

    ホスト名を読み込み、次に適用するオプションの数を読み込みます。各オプションについて、オプションのタイプとその値(複数可)を読み込みます:

    • port: ポート番号が続きます
    • credentials: ユーザー名とパスワードが続きます(2行)
    • maxconn: 最大接続数が続きます
    • timeout: 秒単位のタイムアウトが続きます

    指定されたオプションで DBConnection を作成し、その接続文字列を出力します。

以下の入力が提供されます:

  • 1行目: ホスト名
  • 2行目: オプションの数
  • 続く行: オプションのタイプと値

例えば、以下の場合:

localhost
0

出力は以下のようになります:

admin:secret@localhost:5432 (max:10, timeout:30s)

また、以下の場合:

db.example.com
2
port
3306
credentials
root
mypassword

出力は以下のようになります:

root:mypassword@db.example.com:3306 (max:10, timeout:30s)

さらに、以下の場合:

production.server
4
port
5433
credentials
dbuser
securepass123
maxconn
50
timeout
60

出力は以下のようになります:

dbuser:securepass123@production.server:5433 (max:50, timeout:60s)

呼び出し側が必要なオプションだけを指定し、指定されなかった設定には適切なデフォルト値が使用されていることに注目してください。これが Functional Options パターンの美しさです!

チートシート

Functional Optionsパターンは、長いコンストラクタ引数の代わりに、関数を使用して多くのオプションフィールドを持つ構造体を構成するためのエレガントな方法を提供します。

構造体へのポインタを変更する関数として、Option型を定義します:

type Server struct {
    host    string
    port    int
    timeout int
}

type Option func(*Server)

Optionクロージャを返すオプション関数を作成します:

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 NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,    // デフォルト
        timeout: 30,      // デフォルト
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

使い方はクリーンで柔軟です。呼び出し側は必要なものだけを指定します:

// デフォルトを使用
s1 := NewServer("localhost")

// 特定のオプションをカスタマイズ
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))

利点:デフォルト値が一元管理され、呼び出し側は必要なものだけを指定でき、新しいオプションを追加しても既存のコードが壊れません。

自分で試してみよう

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// ホストを読み込む
	host, _ := reader.ReadString('\n')
	host = strings.TrimSpace(host)

	// オプションの数を読み込む
	numOptionsStr, _ := reader.ReadString('\n')
	numOptions, _ := strconv.Atoi(strings.TrimSpace(numOptionsStr))

	// オプションを収集する
	var opts []Option

	for i := 0; i < numOptions; i++ {
		optionType, _ := reader.ReadString('\n')
		optionType = strings.TrimSpace(optionType)

		switch optionType {
		case "port":
			portStr, _ := reader.ReadString('\n')
			port, _ := strconv.Atoi(strings.TrimSpace(portStr))
			// TODO: WithPort オプションを opts に追加する
			_ = port // 実装時にこの行を削除する

		case "credentials":
			username, _ := reader.ReadString('\n')
			username = strings.TrimSpace(username)
			password, _ := reader.ReadString('\n')
			password = strings.TrimSpace(password)
			// TODO: WithCredentials オプションを opts に追加する
			_, _ = username, password // 実装時にこの行を削除する

		case "maxconn":
			maxStr, _ := reader.ReadString('\n')
			maxConn, _ := strconv.Atoi(strings.TrimSpace(maxStr))
			// TODO: WithMaxConnections オプションを opts に追加する
			_ = maxConn // 実装時にこの行を削除する

		case "timeout":
			timeoutStr, _ := reader.ReadString('\n')
			timeout, _ := strconv.Atoi(strings.TrimSpace(timeoutStr))
			// TODO: WithTimeout オプションを opts に追加する
			_ = timeout // 実装時にこの行を削除する
		}
	}

	// TODO: host と opts を使用して NewDBConnection で DBConnection を作成する
	// TODO: ConnectionString() メソッドを使用して接続文字列を出力する

	_ = opts // 実装時にこの行を削除する
	fmt.Println("TODO: Print connection string here")
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン