はじめに
これまではPHPをメインで扱っていたのですが、最近はGo言語を使用しています。そこで、Go言語ではどのように値オブジェクト&完全コンストラクタパターン(Complete Constructor Pattern)を実装するのか解説していきます。
対象の読者
- Go言語に興味を持っている方
- オブジェクト指向設計(デザインパターン)に興味がある方
- コードの品質向上を目指す方
- PHPからGoへの移行を検討している方
完全コンストラクタパターンの基本
完全コンストラクタパターンとは?
インスタンスを生成した時点で完全な状態となるような初期化ロジックを、コンストラクタに実装するパターンのことです。
完全コンストラクタパターンを用いることで、オブジェクトが不正な状態で存在することがなくなります。これにより、呼び出し側でオブジェクトが正常かどうかチェックする必要がなくなり、ロジックの重複や修正漏れを防ぎ、保守性の向上が見込めます。
注意点 : 生成したインスタンスを不変に保つ
せっかく完全コンストラクタでオブジェクトを完全な状態にしても、不変(immutable)な状態を保たなければ意味がありません。そこで、setterを実装しない&構造体のフィールドは小文字はじまりで非公開にしておきましょう。
※ 構造体のフィールドを小文字始まりにしても同じパッケージ内からはアクセス可能です。外部パッケージからのアクセスのみ制限します。
※ コンストラクタを使わなくても構造体を生成できますが、生成する際は必ずコンストラクタを使用するルールを定めましょう。
Go言語におけるコンストラクタの基本
Go言語ではPHPの__constructのような言語仕様としてのコンストラクタがないため、代わりにNew〜という関数を定義し、その中でインスタンスの生成を行います。
package domain
type User struct {
Name string
Age int
}
// 最小限のコンストラクタ
func NewUser(name string, age int) *User {
return &User{
Name: name,
Age: age,
}
}
func (u *User) GetName() string {
return u.name
}
func (u *User) GetAge() int {
return u.age
}
完全コンストラクタパターンの設計と実装
先ほどのコンストラクタの例では、不完全な状態のオブジェクトを生成できてしまいます。例えば、age
に0を渡してオブジェクトを生成します。
func main() {
user := NewUser("佐藤", -1)
fmt.Println(user.GetAge()) // -1が出力される
}
何もチェックを入れていないので問題なくインスタンスの生成と値の出力ができていますが、これは不完全な状態です。なぜなら本来age
に負の値が入ることはありえないのにも関わらず、インスタンスを生成できてしまっているからです。
そこで、以下のようにUser
を修正します。
package domain
type User struct {
name string
age int
}
func NewUser(name string, age int) (*User, error) {
// nameが空文字列の場合はエラー
if name == "" {
return nil, fmt.Errorf("name is empty")
}
// ageが0未満の場合はエラー
if age < 0 {
return nil, fmt.Errorf("age is negative")
}
return &User{
name: name,
age: age,
}, nil
}
func (u *User) GetName() string {
return u.name
}
func (u *User) GetAge() int {
return u.age
}
コンストラクタ内でage
に負の値が入った場合はエラーを返すようにしており、ついでに名前が空文字列ということは最低限あり得ないので、ここも弾くようにしました。
これにより、NewUser関数を使用する限り不完全な状態でインスタンスが生成されないようになりました。
package main
import "fmt"
func main() {
user, err := NewUser("test user", -1)
if err != nil {
fmt.Println(err) // エラーが出力される
return
}
fmt.Println(user.GetAge())
}
これを実行すると、NewUserがエラーを返すようになり、標準出力としてage is negative
が出力されるようになります。不完全な状態ではインスタンスが生成できないようになっていますね。
終わりに
Go言語で完全コンストラクタパターンの実装を行いました。NewUserでインスタンスを生成するようにしましたが、結局User{}で初期化されてしまうので完全コンストラクタ風にはなっています。これについてはinterfaceを使用することで解決できますが、コード量や複雑さが増すのでトレードオフかと思います。
また、今回記載しているオブジェクトの「完全な状態」とは状況によって異なります。ここはドメインの知識を把握し、しっかりとコードに反映していく必要があります。