こんにちは!株式会社ホワイトプラス、コアシステム開発Gの たなか です。
前回はクリーンアーキテクチャの層の基本:役割・依存方向・実務で効いたポイントという記事を書きました。
クリーンアーキテクチャや DDD(Domain-Driven Design)に触れ始めると、
- Entity
- Value Object(VO)
- DTO
- Published Language(PL)
といった用語が次々に出てきます。
名前は聞いたことがあっても、
- Entity と VO はどう違うのか
- DTO や PL も含めて、どれも「値を入れるだけの入れ物」に見えてしまう
といった形で、頭の中がごちゃごちゃしてしまうことも多いのではないでしょうか。少なくとも私は、当初それぞれの違いや使いどころが分からずよく混乱していました。
本記事では、前回触れたクリーンアーキテクチャの層構造を前提にしつつ、Entity と VO を主役にドメインモデルの考え方を整理していきます。
そのうえで、比較対象として「よく混ざりやすい存在」である DTO と PL との違いにも最後に軽く触れます。
読後のイメージとしては、
- Entity と VO の役割の違いが分かる
- ドメインモデル(Entity / VO)と、境界で使う DTO / PL の役割を分けて考えられる
という状態を目指しています。
クリーンアーキテクチャの層構造のおさらい
前回の記事では、アプリケーション全体をざっくり次のような層に分けて捉える考え方を紹介しました!
- プレゼンテーション層
入力を受け取り、結果を返す(Controller や View 等) - アプリケーション層
処理の流れを組み立て、必要なドメインの操作を指示する - ドメイン層
業務ルールや判断基準をコードで表し、状態や振る舞いの正しさを保証する - インフラ層
DB・API など外部とのやり取りを担い、永続化や通信などを実装する
クリーンアーキテクチャの詳細については、前回の記事で整理していますので、ご興味があればご覧ください。
本記事では、このうち ドメイン層の中身 にフォーカスします。
ドメインモデルとは何か
DDD の文脈でのドメインモデルは、ドメイン(業務)の知識やルール・判断基準をコードで表現したものです。
例えば、顧客情報のドメインであれば、
- どの利用者が「有効な会員」なのか
- 退会済みの利用者をどのように扱うか(ログイン可否や表示の仕方など)
- メールアドレスや電話番号をどのようなルールで管理するか(一意制約、形式、変更のパターンなど)
といったルールや振る舞いがあります。
これらをテキストの仕様書だけに閉じ込めるのではなく、クラス構造やメソッド名、(どこまでを 1 つのかたまりとして扱うかといった)集約の境界など、コード側にも反映していくのがドメインモデルの役割です。
ドメインモデルの中には、代表的なものとして次のような要素が含まれます。
- Entity
- Value Object(VO)
- Domain Service
- Aggregate
- Repository
すべてを一度に扱うと話が散らかってしまうため、本記事ではこの中から、登場頻度が高く、かつ混同されがちな Entity と VO に絞って見ていきます!
Entity の基本
Entity の特徴
Entity は、DDD でよく登場するドメインモデルのひとつです。ざっくり言うと、
名前や住所が変わっても、同じ ID を持っていれば同一人物として扱うような「登場人物」を表すモデル
というイメージです。
主な特徴を整理すると、次のようになります。
- 一意な ID を持つ
user_idやorder_idのような識別子を持ち、その ID で「同じものかどうか」を判断します。
- ライフサイクルを持つ
- ある時点で生成され、その後も同じ ID のまま状態が変化していく時間の流れを持ちます。
- 状態が変わっても「同じもの」として扱う
- 例えば、ユーザーの氏名や会員ステータスが変わっても
user_idが同じであれば同じユーザーです。
- 例えば、ユーザーの氏名や会員ステータスが変わっても
- 業務で「個体として識別したい」存在になりやすい
- 業務要件の中で「誰の」「どの」と特定する必要がある対象です。例えば「どのユーザーの情報を更新するか」「どの注文をキャンセルするか」のように、複数ある中から一つを選んで操作する場面で使われます。
ここで重要なのは、状態が変わることが前提になっているモデルだという点です。
住所や電話番号、会員プランが変わるたびに別ユーザーとして扱うことはありません。
「状態が変わっても同じユーザーとして扱い続ける」ことが業務上必要だからこそ、一意な ID を持つ Entity として表現します。
実務でイメージしやすい例
Web アプリケーションでの典型的な Entity の例として、次のようなものが挙げられます。
User:サービスを利用する人Membership:顧客の契約情報(どのプランで、いつからいつまで有効か)Item:商品情報Order:顧客の注文
現場では「DB のテーブルと 1:1 で Entity を対応させる」パターンもよく見られます。
usersテーブル →Userエンティティmembershipsテーブル →Membershipエンティティitemsテーブル →Itemエンティティordersテーブル →Orderエンティティ
一方で DDD の観点では、
「DB をどう正規化しているか」だけでなく、「業務上、何をひとまとまりの登場人物として扱いたいか」
という視点も重要になります。
例えば、顧客の契約情報が複数テーブルにまたがっていたとしても、
- 業務上の会話では「この顧客はいつからいつまでこのプランです」と 1 まとまりで話している
- 課金や請求ロジックも「この顧客の契約情報」を起点に考えることが多い
といった状況であれば、DB 上は複数行・複数テーブルでも、ドメイン的には 1 つの Membership Entity として扱うのが自然です。
まとめると、Entity は次のような存在を表すモデルだと考えられます。
- ID とライフサイクルを持ち、
- 状態が変わっても「同じもの」として扱いたくて、
- 業務上の「登場人物」に相当するもの
このイメージを持っておくと、後で登場する Value Object との違いも掴みやすくなります。
Value Object の基本
Value Object の特徴
Value Object(以下、VO)も、Entity と並んで頻繁に登場するドメインモデルです。 ざっくり言うと、
「値そのものに意味があり、同じ値であれば同じものとして扱うモデル」
というイメージです。
主な特徴を整理すると、次のようになります。
- ID ではなく「値の中身」で同一性を判断する
- 例えば、同じメールアドレス文字列を持つ
EmailAddressVO 同士は、別々に生成されていても「同じメールアドレス」として扱います。
- 例えば、同じメールアドレス文字列を持つ
- 不変(Immutable)として扱う
- 一度生成した VO の中身は書き換えず、「値が変わるときは新しい VO を作る」という扱い方をします。
- 単なる型エイリアスではなく「意味を持つ」
stringやintのままでは表せない「業務上の意味」や「バリデーションルール」をカプセル化します。
- 「こういう値であってほしい」という制約を内側に閉じ込める
- コンストラクタや生成メソッドの中で、形式や範囲、禁止パターンなどをチェックし、不正な値の生成を防ぎます。
VO は、業務で扱う「値」を安全かつ意図の伝わる形で表現するためのモデルと捉えると分かりやすいと思います。
実務でイメージしやすい例
Web アプリケーションでは、次のようなものが VO になりやすいです。
EmailAddress:メールアドレス- 形式チェック(
@を含む、全体の長さ、ドメインの制約など)を内包し、不正なメールアドレスを表す VO が生成されないようにします。
- 形式チェック(
PhoneNumber:電話番号- 国ごとの形式や桁数、数字以外の文字の扱いなどを VO の内側で統一します。
PostalCode/Address:住所関連の値- 「ハイフンあり/なし」「全角・半角」などの揺れを VO 内で吸収し、コードの他の場所では気にしなくていいようにします。
Amount/Price:金額- マイナス値を禁止する、通貨単位を持たせる、丸めや端数処理のルールを閉じ込める、といったことが可能になります。
いずれも、「DB 上では単なる文字列や数値」になりがちな値ですが、
- その値に固有のルールや前提があり、
- アプリケーションのあちこちで同じようなバリデーションや変換が必要になる
といった場合、VO として切り出す候補になります。
Entity との対比で見る Value Object
Entity との違いを意識すると、VO の性質がより掴みやすくなります。
- 「誰か/どの注文か」といった個体を区別したいときは Entity
- 「どのメールアドレスか」「どの金額か」といった値そのものを扱いたいときは VO
という使い分けが基本のイメージです。
例えば、ユーザーのメールアドレスで考えてみます。
UserEntity は、「このサービスを利用している 1 人のユーザー」という“登場人物”を表す- そのユーザーが持つ
EmailAddressVO は、「そのユーザーの連絡先メールアドレス」という“値”を表す
ユーザーがメールアドレスを変更したい場合、
UserEntity のインスタンスは同じ ID のまま継続し、- その内部で持っている
EmailAddressVO を、古い値から新しい値に差し替えます(新しい VO を生成して入れ替える)。
ここでポイントになるのは、
- Entity:ID で同じかどうかを見る
- VO:中身の値が同じかどうかを見る
という比較の軸が違う、という点です。
この違いを押さえておくと、「これは Entity ではなく VO だな」「ここは VO に切り出したほうがよさそうだ」といった判断がしやすくなります。
DTO / PL の位置づけ
ここまで見てきた Entity / VO は、ドメイン層の中で「業務の登場人物や値・ルールそのもの」を表現するモデルでした。
一方で、実務では DTO(Data Transfer Object)や PL(Published Language)も登場します。
これらも、実装上は Entity や VO と同じように「値を入れておくクラス」や Enum として登場することが多く、クラス定義の見た目やコード上での扱い方だけを見ると、どれも似たような“データの入れ物”に見えてしまいます。
ここでは、立ち位置だけ簡単に整理しておきます。
前提として、当社ではドメイン間の連携に OHS(Open Host Service)、ACL(Anti-Corruption Layer)のパターンを採用しています。
このとき BC 間でやり取りされる「共通言語(契約/規約)」となるのが PL です。
実装レベルでは、この PL を表現するために DTO クラスを定義し、それを JSON などにシリアライズして受け渡す形をとっています。
DTO(Data Transfer Object)
- 境界(層間・BC 間など)でデータを受け渡しする入れ物として使う
- 業務ロジックは持たない
- 持つとしても、配列化・JSON 化やドメインモデルとの相互変換といった補助的な処理にとどめる
PL(Published Language)
- 異なる BC 同士がやり取りするときに使う共通言語という概念
- User BC の内部 Entity をそのまま外に渡さず、「内部のドメインモデル → 公開用の言語」に変換してから他 BC に渡す
例えば、Order BC から User BC へユーザー情報を問い合わせる場合は次のような流れになります。
- Order BC 内の ACL レイヤーからリクエストを受け取り、
- User BC 内の OHS レイヤーで、注文情報が持つ userId など必要な情報を取得し、PL に則った DTO に値をセット
- DTO を Order BC に返して、ACL レイヤーで自ドメインが扱うドメインモデルに組み替える
まとめると、ざっくり次のような関係になります。
- Entity / VO
- 各 BC の中で、業務のルールや判断を表現するモデル
- DTO
- 境界で値を運ぶための入れ物(ロジックは変換・整形などの補助レベルにとどめる)
- PL
- 他の BC から見たときの「公開用のデータ構造」
まとめ
本記事では、クリーンアーキテクチャの層構造を前提に、ドメイン層の中での Entity / VO の役割と、境界側で使う DTO / PL の位置づけを整理しました!
クラスの役割で迷ったときは、次の観点で考えると整理しやすくなります。
- ID で同一性を追いかけたいなら Entity
- 値そのものにルールや一貫性を持たせたいなら VO
- レイヤや BC の境界でデータを運ぶための入れ物なら DTO
- 他 BC から見たときの「公開用のデータ構造・共通言語」なら PL
どれも一見「値を入れる入れ物」に見えますが、こうした観点を一度挟んでみると、「このクラスはどこに置くべきか」が見えやすくなります。
日々の実装やリファクタリングの中で、役割の整理に悩んだときのヒントになれば幸いです!
さいごに
ホワイトプラスでは、ビジョンやバリューに共感していただけるエンジニアを募集しています!
ネットクリーニングの「リネット」など「生活領域×テクノロジー」で事業を展開している会社です。
どんな会社か気になった方は、オウンドメディア「ホワプラSTYLE」をぜひご覧ください。
エンジニアの方向けのエントランスブック「WHITEPLUS Entrance Book for Engineer」もございます。
オンラインでカジュアル面談もできますので、ぜひお気軽にお問い合わせください!