2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Go言語で完全コンストラクタパターンを実装する

Last updated at Posted at 2023-07-31

はじめに

これまではPHPをメインで扱っていたのですが、最近はGo言語を使用しています。そこで、Go言語ではどのように値オブジェクト&完全コンストラクタパターン(Complete Constructor Pattern)を実装するのか解説していきます。

対象の読者

  • Go言語に興味を持っている方
  • オブジェクト指向設計(デザインパターン)に興味がある方
  • コードの品質向上を目指す方
  • PHPからGoへの移行を検討している方

完全コンストラクタパターンの基本

完全コンストラクタパターンとは?

インスタンスを生成した時点で完全な状態となるような初期化ロジックを、コンストラクタに実装するパターンのことです。

完全コンストラクタパターンを用いることで、オブジェクトが不正な状態で存在することがなくなります。これにより、呼び出し側でオブジェクトが正常かどうかチェックする必要がなくなり、ロジックの重複や修正漏れを防ぎ、保守性の向上が見込めます。

注意点 : 生成したインスタンスを不変に保つ

せっかく完全コンストラクタでオブジェクトを完全な状態にしても、不変(immutable)な状態を保たなければ意味がありません。そこで、setterを実装しない&構造体のフィールドは小文字はじまりで非公開にしておきましょう。

※ 構造体のフィールドを小文字始まりにしても同じパッケージ内からはアクセス可能です。外部パッケージからのアクセスのみ制限します。
※ コンストラクタを使わなくても構造体を生成できますが、生成する際は必ずコンストラクタを使用するルールを定めましょう。

Go言語におけるコンストラクタの基本

Go言語ではPHPの__constructのような言語仕様としてのコンストラクタがないため、代わりにNew〜という関数を定義し、その中でインスタンスの生成を行います。

user.go
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を渡してオブジェクトを生成します。

main.go
func main() {
	user := NewUser("佐藤", -1)
	fmt.Println(user.GetAge()) // -1が出力される
}

何もチェックを入れていないので問題なくインスタンスの生成と値の出力ができていますが、これは不完全な状態です。なぜなら本来ageに負の値が入ることはありえないのにも関わらず、インスタンスを生成できてしまっているからです。

そこで、以下のようにUserを修正します。

修正後のuser.go
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関数を使用する限り不完全な状態でインスタンスが生成されないようになりました。

main.go
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を使用することで解決できますが、コード量や複雑さが増すのでトレードオフかと思います。

また、今回記載しているオブジェクトの「完全な状態」とは状況によって異なります。ここはドメインの知識を把握し、しっかりとコードに反映していく必要があります。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?