0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【図解】DDDの頻出用語10個を、Pythonの最小コードで一気に理解する

0
Posted at

はじめに

「DDDに入門しようとしたら、本が分厚すぎて挫折した…」
「エンティティ?集約?リポジトリ?用語が多すぎて、どれが何なのかわからない…」

ドメイン駆動設計(DDD)の学習で最初につまずくのは、設計思想そのものよりも用語の壁です。私も最初はカタカナ用語の多さに圧倒されて、原典を読む手が止まりました。

そこで本記事では、DDDの頻出用語10個を、それぞれ10行前後のPythonコードとセットで一気に解説します。難しい理論は後回しにして、「コードで見ればこういうことか」という直感を先に作るのが狙いです。

この記事を読むと、次の状態になれます。

  • DDDの頻出用語10個を、自分の言葉で説明できる
  • 用語同士の関係(何が何を含むのか)が1枚の図で頭に入る
  • 「DDDをどこまで取り入れるべきか」の判断基準を持てる
  • エヴァンス本などの専門書を読む準備が整う

対象者

  • DDDという言葉は聞いたことがあるが、中身はよく知らない人
  • DDD本を1度開いて閉じたことがある人
  • Pythonの基本文法(クラス、型ヒント)がわかる人
  • 読了目安: 約15分

本記事のコードは Python 3.12 で動作確認しています(dataclassestyping.Protocol を使用します)。

全体像 — 用語は2つのグループに分かれる

DDDの用語は「戦略的設計(システムをどう分割するか)」と「戦術的設計(コードをどう書くか)」の2グループに分けると、一気に見通しが良くなります。

グループ 問い 本記事で扱う用語
戦略的設計 システムをどう分割し、チームでどう言葉を揃えるか ①ユビキタス言語 ②境界づけられたコンテキスト
戦術的設計 分割した中身を、どんな部品で実装するか ③値オブジェクト ④エンティティ ⑤集約 ⑥リポジトリ ⑦ドメインサービス ⑧ドメインイベント ⑨アプリケーションサービス
アンチパターン やってはいけない形 ⑩貧血ドメインモデル

戦術的設計の部品同士は、次の関係になっています。この図を頭に置いて読み進めてください。

① ユビキタス言語 — チーム全員で同じ言葉を使う

定義: 開発者・企画・ドメインエキスパート(業務に詳しい人)が、会話でもコードでも同じ言葉を使うこと。

DDDの出発点は技術ではなく言葉です。会話では「注文をキャンセルする」と言っているのに、コードでは update_status(2) と書かれていたら、仕様とコードの対応が失われます。

bad_naming.py
# Bad: 会話に出てこない言葉でコードが書かれている
order.update_status(2)          # 2って何…?
good_naming.py
# Good: 会話の言葉がそのままコードに現れる
order.cancel()                  # 「注文をキャンセルする」がそのまま読める

ポイント:

  • 用語集を作って会話とコードの言葉を揃える: 「会員?ユーザー?顧客?」の揺れを最初に潰す
  • コードレビューで「この名前、業務の言葉と合ってる?」と問うだけでもDDDの第一歩です

② 境界づけられたコンテキスト — 言葉が通じる範囲で区切る

定義: 1つの用語が1つの意味で通じる範囲を境界として、システムを分割する考え方。

同じ「商品」という言葉でも、販売の文脈では「価格や在庫」、配送の文脈では「重さやサイズ」が関心事です。これを1つの巨大な Product クラスに詰め込むと、あらゆる部署の都合が混ざった神クラス(何でも屋の巨大クラス)が生まれます。

ポイント:

  • 「全部署共通の完璧な商品モデル」を作ろうとしない。文脈ごとに別のモデルを持つ方が変更に強い
  • マイクロサービスの分割単位の議論は、この概念が土台になっています

③ 値オブジェクト — 「値」に名前とルールを与える

定義: IDを持たず、値そのものが本体である不変のオブジェクト。

金額を int のまま扱うと、「マイナスの金額」や「円とドルの足し算」のようなバグを型で防げません。値オブジェクトにすると、ルールが型に染み込みます

money.py
from dataclasses import dataclass

@dataclass(frozen=True)  # frozen=True で不変にするのが値オブジェクトの流儀
class Money:
    amount: int
    currency: str = "JPY"

    def __post_init__(self) -> None:
        if self.amount < 0:
            raise ValueError("金額は0以上である必要があります")

    def add(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError("通貨が異なる金額は加算できません")
        return Money(self.amount + other.amount, self.currency)
>>> Money(1000).add(Money(500))
Money(amount=1500, currency='JPY')
>>> Money(-100)
ValueError: 金額は0以上である必要があります

ポイント:

  • 等価性は「値が同じか」で判断Money(1000) == Money(1000)True
  • 変更したいときは新しいインスタンスを返す(add が新しい Money を返しているのに注目)
  • 「金額」「メールアドレス」「郵便番号」など、検証ルール付きのプリミティブ値はすべて候補です

④ エンティティ — 「同一性」をIDで判断する

定義: 属性が変わっても「同じもの」として追跡したい対象。同一性はIDで判断する。

会員が改名しても、その人は同じ会員です。「値がすべて同じなら同じ」の値オブジェクトとは、等価性の考え方が正反対です。

member.py
from dataclasses import dataclass, field
import uuid

@dataclass(eq=False)
class Member:
    name: str
    id: str = field(default_factory=lambda: str(uuid.uuid4()))

    def rename(self, new_name: str) -> None:
        if not new_name:
            raise ValueError("名前は空にできません")
        self.name = new_name

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Member):
            return NotImplemented
        return self.id == other.id  # 名前が違ってもIDが同じなら同一人物

ポイント:

  • 迷ったら「属性が変わっても追いかけたいか?」と自問する。Yesならエンティティ、Noなら値オブジェクト
  • エンティティの属性はできるだけ値オブジェクトで構成すると、検証ロジックが散らばりません

⑤ 集約 — 整合性を守る「ひとかたまり」

定義: 必ず一緒に整合性を保つべきオブジェクトのまとまり。外部からは代表(集約ルート)経由でしか触らせない。

「注文の合計は10万円まで」というルールがあるとき、注文明細を誰でも直接追加できたら、ルールはすり抜けられます。入口を1つに絞るのが集約です。

order.py
from dataclasses import dataclass, field
from typing import ClassVar

@dataclass(frozen=True)
class OrderLine:                      # 集約の内部部品(値オブジェクト)
    product_name: str
    price: Money

@dataclass
class Order:                          # 集約ルート(エンティティ)
    id: str
    lines: list[OrderLine] = field(default_factory=list)
    LIMIT: ClassVar[Money] = Money(100_000)

    def add_line(self, line: OrderLine) -> None:
        new_total = self.total().add(line.price)
        if new_total.amount > self.LIMIT.amount:
            raise ValueError("注文合計は10万円を超えられません")  # ルールはここで必ず守られる
        self.lines.append(line)

    def total(self) -> Money:
        total = Money(0)
        for line in self.lines:
            total = total.add(line.price)
        return total

ポイント:

  • order.lines.append(...) と直接書かず、必ず order.add_line(...) を通す。これだけで「ルールの抜け道」が消える
  • 集約はトランザクションの単位でもあります。1回の保存で守るべき範囲=1集約が目安
  • 集約を大きくしすぎない。「本当に同時に整合性が必要か?」で切り分けます

⑥ リポジトリ — 永続化の詳細をドメインから隠す

定義: 集約の保存・取得を抽象化する出入り口。ドメイン層はDBの存在を知らない。

order_repository.py
from typing import Protocol

class OrderRepository(Protocol):      # ドメイン層はこのインターフェースだけを知る
    def save(self, order: Order) -> None: ...
    def find_by_id(self, order_id: str) -> Order | None: ...

class InMemoryOrderRepository:        # 実装はインフラ層に置く(テスト用の例)
    def __init__(self) -> None:
        self._store: dict[str, Order] = {}

    def save(self, order: Order) -> None:
        self._store[order.id] = order

    def find_by_id(self, order_id: str) -> Order | None:
        return self._store.get(order_id)

ポイント:

  • ドメイン層のコードに SELECT 文や ORM(Model.objects.filter(...) など)が現れなくなり、業務ルールのテストがDBなしで書ける
  • 本番実装(PostgreSQL版など)とテスト実装(インメモリ版)を差し替えられる
  • リポジトリは集約単位で作ります(OrderLineRepository は作らない)

⑦ ドメインサービス — どのオブジェクトにも属さないルールの置き場

定義: 値オブジェクトにもエンティティにも自然に置けない業務ルールを担うクラス・関数。

「メールアドレスの重複チェック」は、Member 単体では判定できません(他の全会員を知らないため)。こういう「複数のオブジェクトにまたがるルール」の置き場がドメインサービスです。

member_service.py
class MemberService:
    def __init__(self, repository: MemberRepository) -> None:
        self._repository = repository

    def is_duplicated(self, member: Member) -> bool:
        existing = self._repository.find_by_email(member.email)
        return existing is not None and existing != member

迷ったらまずエンティティ・値オブジェクトに置けないか考えてください。何でもドメインサービスに置くと、ロジックがモデルから流出して後述の「貧血ドメインモデル」に近づきます。

⑧ ドメインイベント — 「起きた事実」をオブジェクトにする

定義: ドメインで起きた出来事(過去形の事実)を表す不変オブジェクト。

「注文が確定した、確認メールを送り、在庫を引き当てる」。この「〜たら」をイベントとして切り出すと、注文処理とメール送信を疎結合にできます。

events.py
from dataclasses import dataclass
from datetime import datetime

@dataclass(frozen=True)
class OrderPlaced:                    # 名前は必ず過去形にする
    order_id: str
    total: Money
    occurred_at: datetime

ポイント:

  • イベント名は過去形OrderPlaced / MemberRegistered)。「事実」なので後から変更されない
  • 受け手(メール送信、在庫引当…)を増やしても、注文のコードは変わらない

⑨ アプリケーションサービス — ユースケースの進行役

定義: 「注文する」「退会する」といったユースケース1つを、ドメインの部品を組み合わせて進行させる層。

place_order_usecase.py
class PlaceOrderUseCase:
    def __init__(self, repository: OrderRepository) -> None:
        self._repository = repository

    def execute(self, order_id: str, line: OrderLine) -> None:
        order = self._repository.find_by_id(order_id)   # 1. 取得
        if order is None:
            raise ValueError(f"注文が見つかりません: {order_id}")
        order.add_line(line)                            # 2. ドメインのルールで操作
        self._repository.save(order)                    # 3. 保存

ポイント:

  • アプリケーションサービスは「取得→依頼→保存」の進行役に徹し、業務ルール(if文)を持たないのが理想
  • 業務ルールが書きたくなったら、それは集約かドメインサービスに置くべきサインです
  • FastAPIなどのコントローラからこの execute を呼ぶ構成にすると、Webフレームワークとドメインが分離できます

⑩ 貧血ドメインモデル — 一番よくあるアンチパターン

定義: モデルがgetter/setterだけのデータ入れ物になり、ルールがすべてサービス側に流出した状態。

DDDの用語だけ取り入れると、高確率でこの形になります。Bad→Goodで見比べてください。

anemic_bad.py
# Bad: Orderはただのデータ入れ物。ルールはサービスに散らばる
@dataclass
class Order:
    id: str
    lines: list[OrderLine]

class OrderService:
    def add_line(self, order: Order, line: OrderLine) -> None:
        total = sum(l.price.amount for l in order.lines) + line.price.amount
        if total > 100_000:                  # ルールがモデルの外にある
            raise ValueError("上限超過")     # → 別のサービスが検証を忘れたら終わり
        order.lines.append(line)
rich_good.py
# Good: ルールはOrder自身が持つ(⑤集約のコードと同じ形)
order.add_line(line)   # どこから呼んでも上限ルールが必ず効く

見分け方: モデルのメソッドが get_◯◯/set_◯◯ ばかりで、if 文がサービスクラスに集中していたら貧血です。「データを持つ者がルールも持つ」に寄せていくのが対策です。

マーチン・ファウラーによる原典の解説はこちらです。

DDDを「やりすぎない」ための判断基準

DDDは万能ではありません。次の使い分けが安全です。

状況 DDDの取り入れ度
管理画面などのただのCRUD(作成・読取・更新・削除だけの処理) 不要。ORMのモデルをそのまま使う方が速い
業務ルールが少ないマイクロサービス 値オブジェクトだけ「つまみ食い」する
複雑な業務ルールが中心のシステム 戦術的設計をフル活用する価値あり
複数チーム・複数部署が関わる大規模開発 戦略的設計(コンテキスト分割)から始める

「つまみ食い」は正当な戦略です。値オブジェクト(③)と貧血モデル対策(⑩)だけでも、コードの堅牢さは目に見えて変わります。

まとめ — この3つだけ持ち帰ってください

10個の用語を紹介しましたが、特に重要なのは次の3点です。

  1. 言葉を揃える(①②): 会話の言葉をそのままコードに。1つの意味が通じる範囲でシステムを区切る
  2. ルールをデータの持ち主に置く(③④⑤⑩): 検証ルールは値オブジェクトと集約の中へ。貧血モデルを避ける
  3. ドメインを外界から隔離する(⑥⑨): DBやWebフレームワークの都合をリポジトリとユースケース層で堰き止める

次の一歩

本記事は入口です。体系的に学ぶなら、次の順番をおすすめします。

まずは日本語で書かれた入門書から。本記事の用語が丁寧に再登場します。

その後に原典へ。分厚いですが、用語の直感がある状態なら読み進められます。

原典のエッセンスだけをまとめた公式リファレンス(英語・無料)もあります。

解釈が分かれやすいテーマなので、「うちの現場ではこうしている」「この理解は違うのでは?」などあれば、ぜひコメントで教えてください。この記事は指摘を反映して更新していきます。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?