Goで理解するDDD – 値オブジェクト


ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本」書籍がDDDを実践しはじめるのに分かりやすくとてもよいのですが、忘れてしまい何度か読み直しているのでまとめておきます。

 

値オブジェクトとは

値オブジェクトとは、システム固有の値を表現するために定義されたオブジェクトです。

例えば、氏名をシステムで扱う場合、プリミティブな値で「氏名」を扱うことも可能ですが、値オブジェクトとして扱った方が、扱いやすく表現力も増すということです。

プリミティブな値で表現した場合

func PrintFullName() {
    fullName := "taro suzuki"
    fmt.Println(fullName)
}

値オブジェクトで表現した場合

type FullName struct {
    firstName string
    lastName  string
}

func NewFullName(firstName, lastName string) (*FullName, error) {
    if firstName == "" {
        return nil, errors.New("firstName required")
    }

    if lastName == "" {
        return nil, errors.New("firstName required")
    }

    return &FullName{
        firstName: firstName,
        lastName:  lastName,
    }, nil
}

func (m *FullName) FirstName() string {
    return m.firstName
}

func (m *FullName) LastName() string {
    return m.lastName
}

実行

func PrintFullName() error {
    fullName, err := NewFullName("taro", "suzuki")
    if err != nil {
        return err
    }
    fmt.Println(fullName)
    return nil
}

値オブジェクトには下記の特徴があります。

  • 不変である
  • 交換が可能である
  • 等価性によって比較される

不変である

FullName はシステム固有の値を表している値オブジェクトです。

  • FullName は値
  • FullName は不変にすべき
  • 値を変更するためのふるまいである ChangeLastName メソッドは FullName クラスに定義されるべきものではない

不変であるが交換が可能・・・

どうゆうこと?となりましたが

値オブジェクトはあくまで「値」であり「値の値」を変更するべきではない(そもそも「値の値」なんてものは通常ない)と捉えることでなんとなく腑に落ちました。

交換が可能である

代入こそが値を交換する表現方法です。

つまり値オブジェクトに値オブジェクトを代入することで交換ができます。

fullName := model.NewFullName("taro", "suzuki")
fmt.Println(fullName.FirstName())

// 代入によって交換する
fullName = model.NewFullName("taro", "sato")
fmt.Println(fullName.FirstName())

等価性によって比較される

値オブジェクトはシステム固有の値で、あくまでも値なので、その属性を取り出して比較をするのではなく、値と同じように値オブジェクト同士が比較できるようにする方が自然な記述になります。

func Compare() (bool, error) {
    fullNameA, err := model.NewFullName("taro", "suzuki")
    if err != nil {
        return false, err
    }
    fullNameB, err := model.NewFullName("taro", "suzuki")
    if err != nil {
        return false, err
    }
    // true になる
    return fullNameA == fullNameB, nil
}

値オブジェクトでルールを担保する

フィールドにプリミティブな値を利用していたとしても、値オブジェクトを利用していれば引数として渡された時点でチェックを行えば、ルールの担保ができます。

func NewFullName(firstName, lastName string) (*FullName, error) {
    if firstName == "" {
        return nil, errors.New("firstName required")
    }
    if lastName == "" {
        return nil, errors.New("lastName required")
    }
    return &FullName{
        firstName: firstName,
        lastName: lastName,
    }, nil
}

ふるまいをもった値オブジェクト

値オブジェクトは不変ですが、独自のふるまいが定義できます。下記の処理をプリミティブな値で表現する場合、円とドルを足してしまうなどの不具合を事前にチェックして防ぐことが難しくなります。

振る舞いを持った値オブジェクト例

type Money struct {
    amount   float64
    currency string
}

func NewMoney(amount float64, currency string) *Money {
    return &Money{
        amount:   amount,
        currency: currency,
    }
}

func (m *Money) Add(arg Money) (*Money, error) {
    if m.currency != arg.currency {
        return nil, errors.New("通過単位が異なります。")
    }
    return NewMoney(m.amount+arg.amount, m.currency), nil
}

func (m *Money) Amount() float64 {
    return m.amount
}

実行

func Money() error {
    myMoney := model.NewMoney(1000, "JPY")
    allowance := model.NewMoney(3000, "JPY")
    result, err := myMoney.Add(*allowance)
    if err != nil {
        return err
    }
    fmt.Println(result.Amount())
}

値オブジェクトを採用するモチベーション

  • 表現力が増す
  • 不正な値を存在させない
  • 誤った代入を防ぐ
  • ロジックの散在を防ぐ

まとめ

  • 値オブジェクトのコンセプトは「システム固有の値を作ろう」
  • コードがドキュメントとして機能する、ドメイン駆動設計における基本のパターン
  • ドメインの概念をオブジェクトとして定義しようとするときに、まずは値オブジェクトにあてはめてみることを検討してみる

値オブジェクトについて簡単にまとめましたが、詳しくは下記書籍にてご確認ください。

その他参考書籍


関連記事