🐕

Go言語でEntityを設計するとき個人的に採用しているルール

2024/03/06に公開

Go言語の構造体でEntityを設計する際に採用していたルールを紹介していきます。この記事は、DDD(ドメイン駆動設計)におけるEntityの基本的な理解を前提として書かれています。

開発でEntityをどのようにGo言語の構造体で実装していくべきかについて悩まれている方の指針の一つとなれば幸いです。

ルール

紹介するルールの一覧です。後のセクションで一つ一つ詳しく説明します。

  • 構造体のフィールドは全てプライベートにする。
  • バリデーションは構造体の初期化時に行う。
  • フィールドの値の更新はメソッドを通して行う。
  • 構造体が持つフィールドはMECEにする。

構造体のフィールドは全てプライベートにする

一つ目のルールは、「Entityを表す構造体のフィールドは全てprivateとする」です。これにより、外部パッケージからフィールドの直接的な代入を禁止し、ドメインルールを破るような値を保持してしまうこと防ぎます。

バリデーションは構造体の初期化時に行う

構造体のフィールドを全てプライベートすると、外部パッケージにてEntityのインスタンスを作成するにはコンストラクターやファクトリー等を通して初期化を行うことになります。この初期化処理にバリデーションを実装します。

以下のようなフィールドを持つ User という構造体を考えます。

  • id : 空文字列ではない文字列
  • name : アルファベットのみから構成される文字列

この User のコンストラクターにバリデーションを入れたコード例は次のようになるかと思います。

import (
	"errors"
	"regexp"
)

var alphabetRegex = regexp.MustCompile("^[a-zA-Z]+$")

type User struct {
	id string
	name string
}

func NewUser(id, name string) (*User, error) {
	if id == "" {
		return nil, errors.New("id should not be empty")
	}

	if !alphabetRegex.MatchString(str) {
		return nil, errors.New("name should be alphabetic")
	}

	return &User{id: id, name: name}, nil
}

コンストラクターにはバリデーションを入れず、別途構造体を初期化した後に呼び出すバリデーションのメソッドを用意してやる実装も考えられます。しかし、コンストラクターにバリデーションを入れてやる方が以下のようなメリットがあると考え、私はこちらを採用しています。

  • 生成されたインスタンスはドメインルールを破るフィールドないことが約束される。
    • ただし、パッケージ内からコンストラクターを通さずにインスタンスが生成された場合を除く。
  • バリデーションメソッドの呼び忘れが防止できる。

フィールドの値の更新はメソッドを通して行う

フィールドへのアクセスは、メソッドを通じて行います。これは、フィールドの値の更新にも適用され、フィールドの値を直接変更することを許可しません。これにより、ドメインルールを破るような値がフィールドに代入されることを防ぎます。

例えば、Usernameフィールドの値を更新するためのメソッドは次のようになります。

func (u *User) UpdateName(name string) error {
    if !alphabetRegex.MatchString(name) {
        return errors.New("name should be alphabetic")
    }
    u.name = name
    return nil
}

このUpdateNameメソッドを使うことで、nameフィールドの値の更新は常にバリデーションを通じて行われます。

構造体が持つフィールドはMECEにする

最後のルールは、「フィールドは互いにMECEにする」です。つまり、各フィールドが重複しない情報を持つように設計します。

例えば、「生年月日」と「年齢」のように、同じ情報を示すフィールドを持たないようにします。

type User struct {
    id string
    dayOfBirth time.Time
    age int // dayOfBirthと不整合を起こす可能性がある。
}

年齢を計算するメソッドを用意して改善した場合は以下のようになります。

type User struct {
    id string
    dayOfBirth time.Time
}

func (u *User) calculateAge(now time.Time) int {
    return now.Year() - u.dayOfBirth.Year()
}

これにより、データの冗長性を避け、フィールド間の不整合を防ぐことができます。

まとめ

私が個人的にGo言語の構造体としてEntityを表現する際に採用しているルールをご紹介しました。これからもよりドメインルールを破るバグなどが入りにくいルールを考えていければと思っています。

Discussion