Python | テスト・設計・品質:ドメイン駆動設計

Python Python
スポンサーリンク

ドメイン駆動設計って何?一言でいうと「現実のルールをコードの中心に置く考え方」

ドメイン駆動設計(DDD)は、
「フレームワークやDBの都合ではなく、“現実世界のルール”を中心にコードを組み立てよう」
という設計の考え方です。

ここでいう「ドメイン」は、そのシステムが扱う“問題領域”のことです。
ネットショップなら「注文・在庫・支払い」、勤怠システムなら「出勤・退勤・休暇」などがドメインです。

DDDの一番大事なポイントは、次の二つです。

ビジネス側の言葉(現場の言葉)でモデル(クラス・関数)を作ること
そのモデルの中に「本当に守りたいルール(ビジネスルール)」を閉じ込めること

これができると、コードが「仕様書そのもの」に近づいていきます。


ドメインを「クラスとメソッド」に落とし込む感覚をつかむ

例:ネットショップの「カート」をドメインとして考える

シンプルなネットショップを想像してください。
「カートに商品を入れて、合計金額を計算する」というドメインです。

何も考えずに書くと、こうなりがちです。

def add_to_cart(cart: dict, item_id: int, price: int, quantity: int) -> None:
    if item_id in cart:
        cart[item_id]["quantity"] += quantity
    else:
        cart[item_id] = {"price": price, "quantity": quantity}

def calc_total(cart: dict) -> int:
    total = 0
    for item in cart.values():
        total += item["price"] * item["quantity"]
    return total
Python

動きはしますが、「カート」という概念がバラバラに散らばっています。
ここにDDDの考え方を少し入れてみます。

カートを「ドメインモデル」としてクラスにする

from dataclasses import dataclass

@dataclass
class CartItem:
    item_id: int
    price: int
    quantity: int

    def add(self, quantity: int) -> None:
        self.quantity += quantity


class Cart:
    def __init__(self) -> None:
        self._items: dict[int, CartItem] = {}

    def add_item(self, item_id: int, price: int, quantity: int) -> None:
        if item_id in self._items:
            self._items[item_id].add(quantity)
        else:
            self._items[item_id] = CartItem(
                item_id=item_id,
                price=price,
                quantity=quantity,
            )

    def total(self) -> int:
        return sum(
            item.price * item.quantity
            for item in self._items.values()
        )
Python

ここでやっていることはシンプルですが、意味が大きく変わっています。

「カート」という概念が Cart クラスとしてコード上に現れた
「カートの中の1行」が CartItem として表現された
「カートに商品を追加する」「合計金額を計算する」という振る舞いが、カート自身のメソッドになった

これが「ドメインをモデルとして表現する」というDDDの入り口です。


DDDの超重要キーワード:ユビキタス言語(共通言語)

DDDで一番大事なのは、実は「クラス図」ではなく「言葉」です。
ユビキタス言語(Ubiquitous Language)という考え方があります。

開発者とビジネス側(現場の人)が、
同じ言葉でドメインを話し、その言葉をそのままコードに使う、という考え方です。

例えば、現場でこういう会話がされているとします。

「カートに商品を追加する」
「カートの合計金額を出す」

この言葉を、そのままコードに持ち込みます。

add_item
total

もし現場で「カート行」「明細」と呼んでいるなら、CartItem ではなく LineItem と名付けるかもしれません。

大事なのは、「コードの名前が、現場の会話とズレないこと」です。
これができると、仕様変更の話がそのままコードの変更に対応しやすくなります。

「カートの合計金額に送料を含めたい」
Cart.total の仕様を変える、という話になる

「カート行ごとに割引を入れたい」
CartItem に割引のルールを持たせる、という話になる

この「言葉とコードが直結している状態」が、DDDの一番おいしいところです。


エンティティと値オブジェクトの違いを、Pythonでイメージする

エンティティ:同一性(ID)で区別されるもの

エンティティは、「IDで区別されるもの」です。
ユーザー、注文、カートなどが典型です。

from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    email: str
Python

User は、id が変わらない限り「同じユーザー」です。
名前やメールアドレスが変わっても、「同じ人」として扱います。

値オブジェクト:値が同じなら同じもの

値オブジェクトは、「値が同じなら同じもの」として扱うものです。
お金、期間、メールアドレス、住所などが典型です。

from dataclasses import dataclass

@dataclass(frozen=True)
class Money:
    amount: int
    currency: str

    def add(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError("currency mismatch")
        return Money(amount=self.amount + other.amount, currency=self.currency)
Python

Money(1000, "JPY") が二つあったら、それは区別する必要がありません。
「1000円」という値そのものが重要です。

DDDでは、「これはエンティティか?値オブジェクトか?」を意識してモデリングします。
これによって、どこにルールを持たせるか、どこまで不変にするかが変わってきます。


DDDとテスト・品質の関係を具体的に見る

ドメインモデルは「フレームワーク抜き」でテストできる

さっきの Cart の例をテストしてみます。

def test_cart_total():
    cart = Cart()
    cart.add_item(item_id=1, price=1000, quantity=2)
    cart.add_item(item_id=2, price=500, quantity=1)

    assert cart.total() == 2500
Python

ここには、HTTP も DB も出てきません。
「カートというドメインのルール」だけをテストしています。

DDDでは、ドメインモデルをフレームワークやインフラから切り離して作るので、
こういう「純粋なドメインテスト」が書きやすくなります。

これは品質に直結します。

ビジネスルールの変更を、ドメインモデルのテストで守れる
フレームワークを変えても、ドメインのテストはそのまま使える

「ドメインを中心にテストを書く」という発想が、DDDと相性抜群です。

仕様変更が「ドメインモデルの変更」として見えるようになる

例えば、「カートの合計金額に10%の消費税を含めたい」という仕様変更が来たとします。

DDD的なコードなら、まずこう考えます。

「これはカートのルールだな」
Cart.total の仕様を変えるべきだな

class Cart:
    TAX_RATE = 0.1

    def total(self) -> int:
        subtotal = sum(
            item.price * item.quantity
            for item in self._items.values()
        )
        return int(subtotal * (1 + self.TAX_RATE))
Python

そして、テストもこう変えます。

def test_cart_total_with_tax():
    cart = Cart()
    cart.add_item(item_id=1, price=1000, quantity=2)  # 2000
    assert cart.total() == 2200  # 10%の税
Python

仕様変更が、「どのクラスのどのメソッドを変える話なのか」がはっきりしている。
これがDDDの強さです。


DDDとクリーンアーキテクチャの関係をざっくり整理する

クリーンアーキテクチャとDDDは、よくセットで語られますが、役割が少し違います。

クリーンアーキテクチャ
「層」と「依存の向き」の話。
ドメインを中心に置き、外側にフレームワークやDBを追い出す考え方。

DDD
「ドメインそのものをどうモデル化するか」の話。
エンティティ、値オブジェクト、ドメインサービス、ユビキタス言語など。

ざっくり言うと、

クリーンアーキテクチャは「構造の話」
DDDは「中身(ドメイン)の話」

です。

Pythonでいうと、

クリーンアーキテクチャで「どのモジュールに何を置くか」を決める
DDDで「ドメインのクラスやメソッドをどう設計するか」を決める

という感じで組み合わせると、かなり強い設計になります。


初心者がDDDとどう付き合うといいか

いきなり「エンティティ」「集約」「境界づけられたコンテキスト」など、
DDDの全部を理解しようとすると、ほぼ確実に詰まります。

最初の一歩としては、次の二つだけ意識すれば十分です。

現場の言葉を、そのままクラス名・メソッド名にする
ビジネスルールを、ドメインモデル(クラス)の中に閉じ込める

例えば、今こういうコードがあったら、

def register_user(data: dict) -> None:
    # ここにバリデーションやルールが全部書いてある
    ...
Python

これを少しだけDDD寄りにしてみます。

from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: str

    def validate(self) -> None:
        if not self.name:
            raise ValueError("name is required")
        if "@" not in self.email:
            raise ValueError("invalid email")
Python

そして、外側のコードは「Userを作ってvalidateするだけ」にする。

このくらいの小さな一歩でも、
「ドメインを中心に置く」感覚が少しずつ身についていきます。


まとめ(ドメイン駆動設計は「現場のルールをコードの主役にする」ための考え方)

ドメイン駆動設計を初心者目線でまとめると、こうなります。

ドメイン駆動設計は、「現実世界のルール(ドメイン)を中心に据え、そのルールを表すモデル(クラス・メソッド)をコードの主役にする」設計の考え方。
ユビキタス言語で現場の言葉とコードの名前を揃え、エンティティや値オブジェクトとしてドメインを表現することで、仕様変更が「どのクラスのどのメソッドを変える話か」が見えやすくなる。
ドメインモデルをフレームワークやDBから切り離しておくと、ドメインだけをテストできるようになり、ビジネスルールの変更にも強い、長生きするコードになっていく。

タイトルとURLをコピーしました