Prompts-MCP 686 skills · ai / design / design-pattern / framework / fundamentals / habit / lang / tech-selection /mcp/sse
design-pattern design-pattern/ddd-layering/domain-pure.md medium

ddd-domain-pure

Domain 纯净 — 不依赖框架,封装业务规则。Use when 写 Python 后端代码 / 评审涉及 `domain-pure` 的 PR。

domain值对象业务规则
paths
  • **/domain/**/*.py

DDD · Domain 纯净

规则

Domain 层封装核心业务规则,不依赖任何基础设施(DB / Redis / HTTP / LLM)。

必须满足

形态
仅 Python 内置 + Pydantic + 业务子类 class User(BaseModel): ...
不 import Tortoise / requests / redis
不 import FastAPI / Flask
不读环境变量
可在 pytest 内无外部依赖跑通

示例

# py/domain/credits.py
from dataclasses import dataclass
from datetime import datetime

@dataclass
class Credits:
    base: int           # 基础积分
    bonus: int          # 邀请奖励积分

    @property
    def total(self) -> int:
        return self.base + self.bonus

    def can_afford(self, cost: int) -> bool:
        return self.total >= cost

    def deduct(self, cost: int) -> "Credits":
        """扣减:优先扣 bonus,再扣 base"""
        if not self.can_afford(cost):
            raise InsufficientCredits(need=cost, have=self.total)

        remain = cost
        bonus_deduct = min(self.bonus, remain)
        remain -= bonus_deduct

        base_deduct = remain
        return Credits(base=self.base - base_deduct, bonus=self.bonus - bonus_deduct)


class InsufficientCredits(Exception):
    def __init__(self, need: int, have: int):
        self.need = need
        self.have = have

Credits 是纯领域对象。可以单测:

def test_credits_deduct_bonus_first():
    c = Credits(base=5, bonus=3)
    result = c.deduct(2)
    assert result.bonus == 1   # 先扣 bonus
    assert result.base == 5

def test_credits_deduct_overflow():
    c = Credits(base=5, bonus=3)
    result = c.deduct(7)
    assert result.bonus == 0
    assert result.base == 1

无需 mock 任何东西。

Service 使用

class CreditsService:
    def __init__(self, repo: AccountRepo):
        self.repo = repo

    async def deduct_for_action(self, account_id: str, cost: int) -> None:
        # 1. 加载领域对象
        account = await self.repo.find_or_raise(account_id)
        credits = Credits(base=account.base_credits, bonus=account.bonus_credits)

        # 2. 领域操作
        new_credits = credits.deduct(cost)   # 抛 InsufficientCredits 由 Service 翻译为 ApiException

        # 3. 持久化
        await self.repo.update_credits(account_id, base=new_credits.base, bonus=new_credits.bonus)

反例

# ❌ Domain 里直接查 DB
@dataclass
class User:
    id: int

    async def get_orders(self):
        return await Order.filter(user_id=self.id).all()   # 依赖 Tortoise

# ✅ Domain 只描述数据 + 行为;持久化在 Repo
@dataclass
class User:
    id: int
    name: str

    def is_premium(self) -> bool: ...   # 业务规则

class UserService:
    async def get_orders(self, user: User) -> list[Order]:
        return await self.order_repo.find_by_user(user.id)

小型项目简化

小规模后端可以没有显式 domain/ 目录——业务规则散落在 Service 内。当业务复杂度上升(含核心规则的领域)时再抽 domain/

自检

  • [ ] Domain 类不 import 任何 I/O 库?
  • [ ] Domain 类可单测无需 mock?
  • [ ] 领域异常用业务专有 Exception 子类?
  • [ ] Service 通过 Repo 加载 → 调用 Domain 方法 → Repo 保存?

相关