54
40

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.

DDDをざっくり理解してみる① ~エンティティと値オブジェクト編~

Last updated at Posted at 2022-06-27

はじめに

設計方面の知見が虚無すぎたので、以下書籍を用いて社内でDDDの勉強会を開催することにしました。
この記事は勉強会の内容をまとめたものです。
スタンスとしては初めはざっくりと理解して、回を追う毎に正確に理解していくというものです。
ただざっくりと言っても「さすがに間違っているのでは」というところもあると思うので、是非色々とご指摘いただけたら嬉しいです。

そもそもDDD(Domain Driven Development)ってなんやねん

  • ドメインって何?
    直訳すると「領域」。アプリケーションが解決したい事項を指す。
    例えば家計簿アプリなら「日々の収支を記録したい」になります。
    正しくは「日々の収支を記録する領域」等になるのかも知れませんが、分かりやすさ重視で上記のとおりとします。
    Domain Driven、すなわちドメイン駆動なので、「日々の収支を記録したい」ことにフォーカスして設計をすれば良いわけです。

  • ドメインモデルって何?
    ドメインの登場人物と考えると分かりやすいと思います。
    前述の家計簿アプリであれば、「ユーザー、クレジットカード、銀行口座、お金」などが考えられます。

domain_domainmodel.png

  • 軽くまとめると?
    「ユーザー、クレジットカード、銀行口座、お金」などが「力を合わせて」、「日々の収支を記録」できればDDDと言えそうです。
    まずはドメインモデルをちゃんと作ることを目指します。

  • 分かったような分からないようなだけど、DDDって何のメリットがあるんだろう?
    メリットはシンプルで、やりたいこと(日々の収支を記録したい)とアプリケーションをなるべく近くすることにあると考えています。
    ではどうやって近づけるかと言えば、やりたいこと(=ドメイン)の主要な要素(=ドメインモデル)をプログラムの主役にすることです。
    そうすることで要件に沿った素直なコードが書けるのではないかと考えています。
    よくDDDはオニオンアーキテクチャと併せて使われることが多い印象を受けています。
    オニオンアーキテクチャはインターフェース層やインフラ層等、アプリケーションの責務を細分化しますが、DDDと併せて使用される理由はドメインモデルを守りやすいからだと考えています。
    ドメインモデルにはなるべくドメインの登場人物としての情報だけを詰めたいわけで、SQLを叩いていたり、httpリクエストをパースしたりみたいな処理は書きたくないわけです。
    なのでリクエストを捌くのはインターフェース層へ、SQLを叩くのはインフラ層に譲り、ドメインモデル層をなるべくピュアにできるという話だと考えています。

エンティティと値オブジェクトの話をする前に

  • 一旦DDDのことはすっかり忘れて、フラットな気持ちで次のコードを見てください。構造体を元に会員登録するユーザーを作成し、返却しています。これだけなら著しく問題のあるコードには見えないと思います。
    • Goで書いていますが、構造体とはクラスのようなものだと考えてください。
example.go
type User struct {
    Id int
	Name string
	Age int
}

// 会員登録のAPIだと思ってください
func RegisterUser(name string, age int)(User, error) {
	// 自動生成する前提ですが、便宜上こう書いています。
	id := 0

	if name == "" {
		return User{}, errors.New("ユーザー名が空です")
	}

	if age < 0 {
		return User{}, errors.New("年齢が負の値になっています")
	}
	
    // インスタンス化しています。
	newUser := User{Id: id, Name: name, Age: age}

	return newUser, nil
}
  • ここで仕様追加の連絡が来たとします。成人だけが会員登録できるメソッドを作る必要があるとしましょう。
example.go
type User struct {
    Id int
	Name string
	Age int
}

func RegisterUser(name string, age int)(User, error) {
	id := 0

	if name == "" {
		return User{}, errors.New("ユーザー名が空です")
	}

	if age < 0 {
		return User{}, errors.New("年齢が負の値になっています")
	}

	newUser := User{Id: id, Name: name, Age: age}

	return newUser, nil
}

func RegisterUser2(name string, age int)(User, error) {
	id := 0

    // またバリデーション書くのか・・・。
	if name == "" {
		return User{}, errors.New("ユーザー名が空です")
	}

	if age < 0 {
		return User{}, errors.New("年齢が負の値になっています")
	}

	if age < 18 {
		return User{}, errors.New("年齢が18歳未満です")
	}

	newUser := User{Id: 0, Name: name, Age: age}

	return newUser, nil
}
  • 今後もUser構造体を使うメソッドが増える度に何度も何度もバリデーションを書く必要があります。ですが、何故こんなことになっているかと言うと、本来User構造体が果たすべきバリデーションの責務を呼び出し元のRegisterUserメソッドやRegisterUser2メソッドに書いているからですね。こんな感じにすれば良さそうです。
    • 今回18歳未満の会員登録のパターンもあり得るので、user構造体とNewUserメソッドにその知識を持っているのは違和感があります。そのためregisterUser2メソッドの中に18歳未満のバリデーションが残ってます。
    • 18歳未満の会員登録のパターン色々あるなら、例えばadultUser構造体とNewAdultUserメソッドを作って、そこに18歳以上のみという知識を持たせるのもアリだと思います。
    • GoならadultUser構造体の中にuser構造体を埋め込む形で、Java等ならuser構造体を継承させれば出来そうですね。
example.go
// 小ネタですが、小文字はじまりにするとGoでは外部(同じパッケージ以外)から参照できなくなります。
type user struct {
    Id int
	Name string
	Age int
}

// user構造体をインスタンス化させる特別なメソッドを用意する。
// こちらは大文字で始めているので、外部からは常にこのメソッドを使ってインスタンス化することを強制できます。
func NewUser(name string, age int)(User, error) {
	id := 0

	if name == "" {
		return user{}, errors.New("ユーザー名が空です")
	}

	if age < 0 {
		return user{}, errors.New("年齢が負の値です")
	}
	
	return user{Id: id, Name: name, Age: age}
}

func RegisterUser(name string, age int)(User, error) {
	id := 0

    // user構造体を使いたい時はこのメソッドを使う。バリデーションはNewUserメソッドがやってくれる。
	newUser, err := NewUser(name, age)

	if err != nil {
		return newUser, err
	}

	return newUser, nil
}

func RegisterUser2(name string, age int)(User, error) {
	id := 0

    if age < 18 {
		return User{}, errors.New("年齢が18歳未満です")
	}

	newUser := NewUser(name, age)

	if err != nil {
		return newUser, err
	}

	return newUser, nil
}
  • 無意識の内にこんなメソッドを書いたことがある方もいらっしゃると思います。「単にファクトリメソッド作っただけでしょ?」と思われるかも知れませんが、お伝えしたかったことはuserに関する知識を集約することが出来たということです。こういった知識の集約が出来たものを知識集約体と呼びたいと思います。
example.go
// userとNewUserを合わせて知識集約体と呼ぶことにします。
type user struct {
    Id int
	Name string
	Age int
}

func NewUser(name string, age int)(User, error) {
	id := 0

	if name == "" {
		return user{}, errors.New("ユーザー名が空です")
	}

	if age < 0 {
		return user{}, errors.New("年齢が負の値です")
	}
	
	return user{Id: id, Name: name, Age: age}
}

まずエンティティをざっくり理解する

  • やっと本題です。ドメインモデルはドメインの登場人物と言いました。家計簿アプリではユーザーはドメインモデルの一つと言えそうでした。
  • そして先ほどまで見てきたuser構造体とNewUserメソッドはユーザーを表現できていそうです。
  • エンティティとは先ほどのuser構造体とNewUserメソッドのような知識集約体を指します。
  • つまりドメインモデル=エンティティと言えそうです。
    • 正しい理解ではないのですが、一旦この理解で進めてください。

まず値オブジェクトをざっくり理解する

  • エンティティとはuser構造体とNewUserメソッドのような知識集約体であると述べました。値オブジェクトとは何かを考える前に次のような状況を考えてください。ちょっと無理やりバリデーション増やしてますが、説明のためなのでご容赦ください。
example.go
type user struct {
    Id int
	Name string
	Age int
}

func NewUser(name string, age int)(User, error) {
	id := 0

    // nameのバリデーションが多い・・・!
	if name == "" {
		return user{}, errors.New("ユーザー名が空です")
	}

    if utf8.RuneCountInString(name) < 3 {
		return user{}, errors.New("ユーザー名は4文字以上です")
    }

    if utf8.RuneCountInString(name) > 100 {
		return user{}, errors.New("ユーザー名は100文字以内です")
    }

    // ageのバリデーションもあるのか・・・!
	if age < 0 {
		return user{}, errors.New("年齢が負の値です")
	}
	
	return user{Id: id, Name: name, Age: age}
}
  • NewUserメソッドの中にバリデーションが4つあり、その内3つはnameプロパティのもの、1つはageプロパティのものです。今後もnameプロパティのバリデーションは増えそうで、数字が入っていたりするとアウトという条件も追加になりそうという状況だとします。
  • この時、user構造体の知識をNewUserに詰め込んだように、nameプロパティの知識も同様に詰め込みたいと思いませんでしょうか?
  • このname構造体は一つの値しか持っていません。このようなものを値オブジェクトと呼びます。
    • 「ん?でもエンティティの中から2つの関連するプロパティを抜き出したくなることあるんじゃない?」と思った方は鋭いです。今後執筆予定の別記事でご説明しますのでお待ちください。(例えばエンティティ(ユーザー)の中に苗字と名前というプロパティがあり、それをフルネームというオブジェクトに切り出したい。それって2つ値があるから値オブジェクトじゃなくね?、っていう話です)
example.go
// nameプロパティを構造体にしてしまう。
type name struct {
	Value string
}

// 知識を集約する。
func NewName(name string) (name, error) {
	if name == "" {
		return name{}, errors.New("ユーザー名が空です")
	}

	if utf8.RuneCountInString(name) < 3 {
		return user{}, errors.New("ユーザー名は4文字以上です")
	}

	if utf8.RuneCountInString(name) > 100 {
		return user{}, errors.New("ユーザー名は100文字以内です")
	}

	return name{Value: name}, nil
}

type user struct {
    Id int
	Name string
	Age int
}

func NewUser(name string, age int) (User, error) {
	id := 0

    // 呼び出し元でエラーハンドリングするだけで良い。
	newName, err := NewName(name)

	if err != nil {
		return user{}, err
	}

	if age < 0 {
		return user{}, errors.New("年齢が負の値です")
	}

	return user{Id: id, Name: newName, Age: age}
}

まとめ

  • ドメインとはソフトウェアを使う人が解決したい課題のこと。
  • ドメインモデルとはその課題の登場人物のこと。
  • エンティティとはドメインモデルをコードで表現したもの。
    • エンティティ = ドメインモデルでとりあえずOK。
  • 値オブジェクトとはエンティティの一部のプロパティを切り出して知識を持たせたもの。

まとめの図
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3937333438322f30303265643936312d376663312d333339302d373235662d6535353137636136363934662e706e67.png

おわりに

続編はこちら。

54
40
1

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
54
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?