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か所で明確になり、新しいオプションを追加しても既存のコードを壊さないからです。grpcやzapのようなGoライブラリで広く使われています。
チャレンジ
簡単Functional Options Pattern を使って、設定可能な database connection を構築しましょう!このパターンは、妥当なデフォルト値を持つ多くの任意設定がある場合に力を発揮します。まさに database connection に必要なものです。
コードを次の2つのファイルに分けて整理します。
database.go:database connection の型と option 関数を Define します。次の非公開フィールドを持つ
DBConnectionstruct を作成します。host(string)port(int)username(string)password(string)maxConnections(int)timeout(int)- 秒単位
*DBConnectionを modifies する function としてOptiontype を Define します。Optionを返す次の option function を作成します。WithPort(port int)WithCredentials(username, password string)WithMaxConnections(max int)WithTimeout(seconds int)
*DBConnectionを返す constructorNewDBConnection(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 numbercredentialsの後に username と password(2行)maxconnの後に max connections numbertimeoutの後に秒単位の 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")
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー自分で練習してみよう: Goオンラインコンパイラ