Menu
Coddy logo textTech

Functional Options パターン

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

多くの省略可能な設定フィールドを持つ構造体を作成する場合、コンストラクター関数は扱いにくくなることがあります。Functional Options Patternは、関数を使ってオブジェクトを設定することで、この問題を洗練された形で解決します。

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

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))

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

challenge icon

チャレンジ

簡単

Functional Options Pattern を使って、設定可能な database connection を構築しましょう!このパターンは、妥当なデフォルト値を持つ多くの任意設定がある場合に力を発揮します。まさに database connection に必要なものです。

コードを次の2つのファイルに分けて整理します。

  • database.go:database connection の型と option 関数を Define します。

    次の非公開フィールドを持つ DBConnection struct を作成します。

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

    *DBConnection を modifies する function として Option type を Define します。

    Option を返す次の option function を作成します。

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

    *DBConnection を返す constructor NewDBConnection(host string, opts ...Option) を作成します。次のデフォルト値を設定します。port は 5432、username は "admin"、password は "secret"、maxConnections は 10、timeout は 30 です。デフォルト値を設定した後、指定されたすべての option を Apply します。

    connection info を次の Format で返す ConnectionString() method を追加します。

    [username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)
  • main.go:さまざまな設定で database connections を構築します。

    host を読み取り、次に Apply する options の数を読み取ります。各 option について、option の種類とその値を読み取ります。

    • port の後に port number
    • credentials の後に username と password(2行)
    • maxconn の後に max connections number
    • timeout の後に秒単位の timeout

    指定された options で DBConnection を Create し、その connection string を出力します。

次の入力が提供されます。

  • 1行目:Host name
  • 2行目:options の数
  • 続く行:Option の種類と値

たとえば、次の入力が given された場合:

localhost
0

出力は次のようになります。

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

また、次の入力が given された場合:

db.example.com
2
port
3306
credentials
root
mypassword

出力は次のようになります。

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

さらに、次の入力が given された場合:

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

出力は次のようになります。

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

呼び出し側は必要な options だけを指定し、指定されていない設定には妥当なデフォルト値が使用されることに注目してください。これが Functional Options Pattern の優れた点です!

自分で試してみよう

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腕試し

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

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

自分で練習してみよう: Goオンラインコンパイラ