Golangで構造体の初期化をしようとすると、しばしFunctionalOptionパターンというデザインパターンの記事を見かけます。
Javaで開発してきた人にとって構造体の初期化はBuilderパターンを用いることが多いためあまり馴染みのない言葉なのではないでしょうか?
今回はGolangの特性を活かしたFunctionalOptionパターンが何なのか説明していきます。
FunctionalOptionパターンとは
オブジェクトの構成や初期化時に柔軟性と読みやすさを提供するために使われるデザインパターンです。特にGo言語で広く利用されています。
最大のメリットは、FunctionalOptionパターンを使用すると、クライアントコードは必要なオプションのみを指定して構造体の初期化をすることができるところにあります。
また構成が非常にシンプルで、可読性が高いといった特徴があります。
基本的なアプローチ
結論としては以下のようなコードです。ユーザー情報の構造体を初期化する処理を想定したコードです。
実装手順については後ほど説明いたします。
【参考資料】
package main
import "fmt"
// ユーザー情報構造体
type User struct {
UserID string
UserName string
Email string
}
// FunctionalOption型
type UserOption func(*User)
// 各オプション関数
func WithUserID(userID string) UserOption {
return func(u *User) {
u.UserID = userID
}
}
func WithUserName(userName string) UserOption {
return func(u *User) {
u.UserName = userName
}
}
func WithEmail(email string) UserOption {
return func(u *User) {
u.Email = email
}
}
// コンストラクタ
func NewUser(opts ...UserOption) *User {
user := &User{}
for _, opt := range opts {
opt(user)
}
return user
}
func main() {
user := NewUser(
WithUserID("hogehoge"),
WithUserName("山田 花子"),
WithEmail("hanako@example.com"),
)
fmt.Printf("%+v\n", user)
}
この例では、User構造体の各フィールドに対して、それを設定する以下の関数を定義しています。
- WithUserID
- WithUserName
- WithEmail
NewUser関数では、任意の数のUserOption関数を受け取り、それらを適用してUserインスタンスを初期化します。
実装手順
初期化したい構造体と「FunctionalOption型」を定義:
以下の型をまずは定義します。
①初期化したい構造体(ユーザー構造体)
②ユーザー構造体のインスタンスを変更するための関数を表す型エイリアスとして機能する型(FunctionalOption型)を定義します。
// ユーザー情報構造体
type User struct {
UserID string
UserName string
Email string
}
// FunctionalOption型
type UserOption func(*User)
オプション関数の定義:
オプションとして設定できる各プロパティに対して、そのプロパティを設定するための関数を定義します。これらの関数は、オブジェクトのポインタを変更する関数を返します。上記コードの以下の箇所です。
// 各オプション関数
func WithUserID(userID string) UserOption {
return func(u *User) {
u.UserID = userID
}
}
func WithUserName(userName string) UserOption {
return func(u *User) {
u.UserName = userName
}
}
func WithEmail(email string) UserOption {
return func(u *User) {
u.Email = email
}
}
コンストラクタ関数の定義:
オブジェクトを初期化するためのコンストラクタ関数を定義します。この関数は、オプション関数のスライスを引数に取り、それらを適用することでオブジェクトの初期化を行います。
// コンストラクタ
func NewUser(opts ...UserOption) *User {
user := &User{}
for _, opt := range opts {
opt(user)
}
return user
}
↓コンストラクタの引数に各オプション関数が設定されている
func main() {
user := NewUser(
WithUserID("hogehoge"),
WithUserName("山田 花子"),
WithEmail("hanako@example.com"),
)
fmt.Printf("%+v\n", user)
}
なぜFunctionalOptionパターンを使うのか
結論、Golangの言語特性を活かしたデザインパターンだからです。
では、どこがGolangの言語特性を活かせているのでしょうか。
-
シンプルさ:
まず、Golangの関数とクロージャ(関数内で定義した関数)を活用して、複雑な初期化ロジックを簡潔に表現できることが挙げられます。これにより、Golangの「少ない概念で多くを成し遂げる」という設計哲学に沿うことができます。 -
拡張性:
新しいオプションを追加する際に、既存のコードを変更する必要がなく、互換性を保ちながら機能拡張が可能です。 -
オプショナルなパラメータ:
関数のシグネチャを変更せずに、オプショナルなパラメータを柔軟に扱うことができます。特にGolangでは同じ名前の関数を複数定義することができず、それぞれの関数はユニークな関数名とそのパラメータリスト(シグネチャ)を持つ必要があるため、特に適しています。
FunctionalOptionパターンのデメリット
以下のデメリットが挙げられます。コードを読んでいてお気づきの方もいらっしゃるかもしれません。
-
複雑な構成には不向き:
複数のステップを通じて複雑な構成のオブジェクトを段階的に構築する場合、FunctionalOptionパターンは管理が難しくなる。 -
型安全性の欠如:
コンパイル時にオプション関数の適用忘れや誤用を検出できないため、実行時エラーのリスクがある。(気づかないかも。。)
まとめ
私の現場では、Builderパターンを用いて開発していたのであまり馴染みはなかったのですがFunctionalOptionパターンが効果的な場面は積極的に設計していきたいと思いました。(アーキテクト側に回れればの話ですが。。)
ちなみにUber社が出している。GoのコーディングガイドラインでもFunctionalOptionパターンを採用しているみたいです。↓これです
この記事が、誰かの役に立てば幸いです。