ドメインモデルとは何か
ドメインモデルは
「扱っている世界(業務)の“概念”を、そのままクラスとして表現したもの」
です。
給料計算システムなら「社員」「勤怠」「給与」
EC サイトなら「商品」「カート」「注文」「在庫」
銀行なら「口座」「残高」「振込」「手数料」
そういった「そのシステムが相手にしている世界」を、
クラスやメソッド、フィールドとしてモデル化したものがドメインモデルです。
ポイントは、技術用語ではなく「業務の言葉」でクラスを設計することです。
「Dto」「Manager」「Util」といった“技術寄りの名前”ではなく、
「Order(注文)」「Money(金額)」「Stock(在庫)」のような“現実世界の概念”をクラスにします。
ドメインモデルを支える基本要素
エンティティ(同一性を持つもの)
エンティティは「同一性(ID)を持つオブジェクト」です。
例えば「注文」は、同じ商品・同じ金額でも、
注文番号が違えば別物として扱います。
こういった「ID で区別されるもの」がエンティティです。
Java だと、だいたいこんな形になります。
final class OrderId {
private final String value;
OrderId(String value) { this.value = value; }
String value() { return value; }
}
final class Order {
private final OrderId id;
// 他にも、顧客情報や合計金額など
Order(OrderId id /*, ... */) {
this.id = id;
}
OrderId id() { return id; }
}
Java「同じかどうか」は ID で決める、というのがエンティティの考え方です。
値オブジェクト(値として扱うもの)
値オブジェクトは、「中身が同じなら同じものとみなす、小さな概念」です。
金額、メールアドレス、商品名、個数などが典型例です。int や String のまま扱うと意味がぼやけるので、
その意味ごとクラスにしてしまいます。
final class Money {
private final int amount; // 通貨は簡略化
Money(int amount) {
if (amount < 0) throw new IllegalArgumentException("マイナスは不可");
this.amount = amount;
}
Money add(Money other) {
return new Money(this.amount + other.amount);
}
int amount() { return amount; }
}
Java「金額を int で持つ」ではなく「Money として扱う」ことで、
「マイナス禁止」「単位の取り違え防止」などのルールを一箇所に閉じ込められます。
ドメインサービス(誰のものでもないロジック)
「社員でも注文でもないけれど、業務としては重要な処理」は、
ドメインサービスとしてクラスにします。
例えば「振込」「送料計算」「在庫引当」など、
複数のエンティティや値オブジェクトにまたがるルールは、
無理にどれか一つのエンティティに押し込めず、サービスとして切り出します。
final class ShippingCostService {
Money calculate(DeliveryAddress address, Cart cart) {
// 住所やカートの中身を見て送料を計算する
// ここには「送料ルール」というドメイン知識だけを書く
}
}
Javaエンティティは「自分に強く関係する振る舞い」を持ち、
それ以外の「横断的な業務ルール」はドメインサービスに置く、というイメージです。
具体例:EC サイトのドメインモデル
商品と金額の値オブジェクト
EC サイトを例にします。
素朴に書くと、こうなりがちです。
final class Item {
String name;
int price; // 金額がただの int
}
Javaこれだと「price が税込か税抜か」「通貨は何か」がコードからは伝わりません。
また、int 同士を足し算・引き算しているうちに、
税抜と税込を混ぜるといったバグが起きやすくなります。
ドメインモデル的に書くなら、こうします。
final class ItemName {
private final String value;
ItemName(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("商品名は必須");
}
this.value = value;
}
String value() { return value; }
}
final class Money {
private final int amount; // ここでは通貨は省略
Money(int amount) {
if (amount < 0) throw new IllegalArgumentException("金額は0以上");
this.amount = amount;
}
Money add(Money other) {
return new Money(this.amount + other.amount);
}
int amount() { return amount; }
}
final class Item {
private final ItemName name;
private final Money price;
Item(ItemName name, Money price) {
this.name = name;
this.price = price;
}
ItemName name() { return name; }
Money price() { return price; }
}
Java「商品名」「金額」という“意味”をクラスで表現しています。
これがドメインモデルの一番基本的な形です。
注文エンティティと不変条件(ビジネスルール)
注文は、「明細の集合」として表せます。
final class OrderLine {
private final Item item;
private final int quantity;
OrderLine(Item item, int quantity) {
if (quantity <= 0) throw new IllegalArgumentException("数量は1以上");
this.item = item;
this.quantity = quantity;
}
Money lineTotal() {
return new Money(item.price().amount() * quantity);
}
}
Java注文本体はこうなります。
final class Order {
private final OrderId id;
private final List<OrderLine> lines;
Order(OrderId id, List<OrderLine> lines) {
if (lines == null || lines.isEmpty()) {
throw new IllegalArgumentException("明細が1件もない注文は無効");
}
this.id = id;
this.lines = List.copyOf(lines);
}
Money totalPrice() {
Money total = new Money(0);
for (OrderLine line : lines) {
total = total.add(line.lineTotal());
}
return total;
}
OrderId id() { return id; }
List<OrderLine> lines() { return lines; }
}
Javaここで大事なのは、
「明細が 0 件の注文はそもそも存在してはいけない」
というビジネスルールをコンストラクタで保証していることです。
これがドメインモデルの核心の一つです。
「業務として成立しない状態のオブジェクトは、そもそも作らせない」という方針です。
なぜドメインモデルで設計するのか(重要なポイント)
ドメインモデルの狙いは、「業務の複雑さをコードの中にうまく閉じ込めて、扱いやすくすること」です。
データクラスにして、全部 Service クラスで if 書きまくり、という書き方をすると、
ビジネスルールがコード全体に散らばります。
どこか 1 か所でルールが変わったときに、
「どの if を直せばいいのか」「どこまで影響するのか」が非常に分かりづらくなります。
ドメインモデルでは
金額のルールは Money に
注文の整合性は Order に
送料のルールは ShippingCostService に
というふうに、「どのクラスがどのルールを持つか」をはっきりさせます。
結果として、
どこを読めば、この業務ルールが分かるか
どこを直せば、この仕様変更に対応できるか
が明確になります。
さらに、テストも「ルール単位」で書けます。
Money 単体で「マイナス拒否」「加算の結果」をテスト
Order 単体で「0 件不可」「合計金額の計算」をテスト
というように、ドメインモデルを単体テストの単位として扱えるようになります。
これが、長期的に見てコードベースの寿命を伸ばす大きな力になります。
ドメインモデルをコードに落とし込むステップ
業務の言葉をそろえる
まずは「業務で使っている言葉」を丁寧に拾います。
顧客、注文、見積、請求書
予約、キャンセル、チェックイン
入庫、出庫、在庫
こうした言葉が、そのままクラス名になります。
逆に「Service」「Manager」「Util」ばかり出てくるなら、
まだドメインの言葉が十分に出てきていないサインです。
値オブジェクトに切り出す
次に、「ただの int や String にしているけれど、本当は意味のある値」を見つけて、
値オブジェクトとして切り出します。
金額、数量、メールアドレス、電話番号、郵便番号、年月日、期間などです。
一つずつクラスにすると、最初は少し面倒に感じるかもしれませんが、
慣れてくると「バグを防ぐための守り」をコードに埋め込んでいる感覚になります。
振る舞いをエンティティやサービスに寄せる
if 文の中で「ドメインの判断」をしているコードを見つけたら、
それをエンティティや値オブジェクト、ドメインサービスのメソッドに移せないか考えます。
たとえば
if (orderLines.isEmpty()) { ... }
Javaというチェックがいろんな場所にあるなら、Order のコンストラクタで「空の注文はそもそも禁止」にしてしまう。
if (email == null || !email.contains("@")) { ... }
Javaというチェックが散っているなら、EmailAddress クラスのコンストラクタで検証してしまう。
こうすることで、「ビジネスルールが流れ作業の if ではなく、
“名指しできるクラス”の中に集まっていく」状態を作れます。
まとめ:ドメインモデルは「業務の頭の中」をコードに写す作業
ドメインモデルの本質は、
「そのシステムが相手にしている世界の概念とルールを、クラスやメソッドとして写し取ること」です。
テーブル構造や API 仕様から逆算してクラスを作るのではなく、
まず「業務の言葉」と「業務のルール」を中心に置いて、それを Java で表現していきます。
