【Python工程化实战】领域驱动设计(DDD)在 Python 中的落地实践

在 Python 中落地领域驱动设计(DDD)的核心在于:避免“数据库驱动开发”,转向以领域模型为中心的设计。正确结合 Pydantic 与 Python 原生类(dataclass/普通类),可以有效实现限界上下文划分、聚合根与值对象的构建,并提升系统的可维护性、可扩展性与业务逻辑表达能力。

需要特别强调的是:Pydantic 应作为边界层的验证与序列化工具,而非领域模型的基类。聚合根应使用原生 Python 类封装行为与不变性,Pydantic 则负责 API 输入输出与持久化映射。

以下是修正后的完整实践指南,包括关键设计模式与示例代码。


一、限界上下文划分(Bounded Context)

DDD 强调将复杂系统拆分为多个限界上下文,每个上下文有独立的责任边界和语言模型。在 Python 中,通常使用包或模块结构划分上下文,并严格遵循依赖倒置原则。

示例:一个电商系统的推荐目录结构:

project_root/
├── domain/                # 领域层(纯Python,无外部依赖)
│   ├── orders/            # 订单限界上下文
│   │   ├── __init__.py
│   │   ├── order.py       # 聚合根 + 实体
│   │   ├── events.py      # 领域事件定义
│   │   └── repository.py  # 仓储抽象接口
│   ├── customers/         # 客户限界上下文
│   └── products/          # 商品限界上下文
├── application/           # 应用服务层
│   ├── dto.py             # Pydantic DTO(输入/输出模型)
│   └── order_service.py   # 应用服务
├── infrastructure/        # 基础设施层
│   ├── persistence/       # 仓储具体实现(ORM/NoSQL)
│   └── messaging/         # 消息总线实现

关键点:每个上下文的 repository.py 只定义抽象接口(Protocol/ABC),具体实现在 infrastructure 层,确保领域层不依赖任何框架。


二、聚合根与值对象建模

1. 值对象(Value Object)

值对象通过字段定义其语义,必须保证不可变性。金融相关字段必须使用 Decimal,严禁使用 float

from decimal import Decimal
from pydantic import BaseModel, field_validator

class Money(BaseModel):
    """值对象:金额(不可变)"""
    amount: Decimal  # ✅ 必须使用 Decimal,避免浮点精度丢失
    currency: str = "CNY"

    model_config = {"frozen": True}  # ✅ 值对象必须不可变

    @field_validator('amount')
    @classmethod
    def validate_amount(cls, v: Decimal) -> Decimal:
        if v <= 0:
            raise ValueError("金额必须大于0")
        return v

    def __add__(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError(f"币种不一致: {self.currency} vs {other.currency}")
        return Money(amount=self.amount + other.amount, currency=self.currency)

2. 聚合根(Aggregate Root)

聚合根是业务中不可再分的最大实体,负责维护内部不变性。不应继承 Pydantic BaseModel,而应使用 dataclass 或普通类,将业务行为封装在方法中。

from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from typing import List
from decimal import Decimal

@dataclass
class OrderItem:
    product_id: str
    quantity: int
    unit_price: Decimal

    @property
    def subtotal(self) -> Decimal:
        return self.unit_price * self.quantity

@dataclass
class OrderAggregate:
    """
    ✅ 聚合根:使用 dataclass 而非 BaseModel
    - 封装业务行为,保护不变性
    - 收集领域事件,不与序列化耦合
    """
    id: str
    customer_id: str
    items: List[OrderItem] = field(default_factory=list)
    status: str = "PENDING"
    _domain_events: list = field(default_factory=list, repr=False)

    # ✅ 通过工厂方法创建,而非直接构造
    @classmethod
    def create(cls, customer_id: str, items: List[OrderItem]) -> OrderAggregate:
        if not items:
            raise ValueError("订单至少包含一个商品")
        order = cls(
            id=str(uuid.uuid4()),
            customer_id=customer_id,
            items=items,
            status="PENDING"
        )
        # ✅ 注册领域事件
        from .events import OrderCreated
        order._domain_events.append(
            OrderCreated(order_id=order.id, total_amount=order.total)
        )
        return order

    @property
    def total(self) -> Decimal:
        """✅ 计算属性由聚合根自身维护,不依赖外部验证器"""
        return sum((item.subtotal for item in self.items), Decimal("0"))

    def confirm(self) -> None:
        """✅ 业务行为封装在聚合根内部"""
        if self.status != "PENDING":
            raise ValueError(f"只有待处理订单可确认,当前状态: {self.status}")
        self.status = "CONFIRMED"
        from .events import OrderConfirmed
        self._domain_events.append(OrderConfirmed(order_id=self.id))

    def collect_events(self) -> list:
        """提取并清空领域事件,供应用服务发布"""
        events = self._domain_events.copy()
        self._domain_events.clear()
        return events

为什么不用 Pydantic BaseModel 做聚合根?

  • Pydantic 模型默认是数据容器,鼓励外部直接赋值,破坏封装性;
  • field_validator 是声明式验证,无法表达复杂的跨字段业务规则与状态流转;
  • 聚合根需要 collect_events()、状态机等行为,与 Pydantic 的序列化职责冲突;
  • Pydantic 应用于 API 层 DTO 和持久化映射,而非领域模型本身。

三、应用服务层与事务边界

聚合根不应直接暴露于 HTTP 层,应通过应用服务协调业务流程。仓储接口定义在领域层,实现在基础设施层。

# domain/orders/repository.py
from typing import Protocol, Optional
from .order import OrderAggregate

class OrderRepository(Protocol):
    """✅ 仓储接口定义在领域层(依赖倒置)"""
    def save(self, order: OrderAggregate) -> None: ...
    def find_by_id(self, order_id: str) -> Optional[OrderAggregate]: ...
# application/order_service.py
from decimal import Decimal
from typing import List
from domain.orders.order import OrderAggregate, OrderItem
from domain.orders.repository import OrderRepository

class OrderService:
    def __init__(self, repo: OrderRepository):
        self.repo = repo

    def create_order(
        self,
        customer_id: str,
        items: List[dict],
    ) -> OrderAggregate:
        # ✅ 输入验证建议用 Pydantic DTO,此处简化演示
        order_items = [
            OrderItem(
                product_id=i["product_id"],
                quantity=i["quantity"],
                unit_price=Decimal(str(i["unit_price"]))  # ✅ float→Decimal
            )
            for i in items
        ]

        # ✅ 通过聚合根工厂方法创建,而非在服务层拼装
        order = OrderAggregate.create(customer_id=customer_id, items=order_items)

        # ✅ 保存并发布事件
        self.repo.save(order)
        events = order.collect_events()
        # TODO: 发布事件到消息总线
        return order

四、领域事件机制

领域事件应定义为独立的不可变数据类,与聚合根解耦。

# domain/orders/events.py
from dataclasses import dataclass
from decimal import Decimal

@dataclass(frozen=True)
class OrderCreated:
    order_id: str
    total_amount: Decimal

@dataclass(frozen=True)
class OrderConfirmed:
    order_id: str

关于事件溯源:初学者建议先采用「状态存储 + 领域事件发布」模式。真正的事件溯源需要按序存储所有事件、通过重放重建状态、配合快照优化,复杂度较高,不建议在未充分理解时直接使用。


五、Pydantic 的正确定位:边界防腐层

Pydantic 在 DDD 中的正确角色是边界层的验证与序列化工具,而非领域模型本身:

# application/dto.py — ✅ Pydantic 用于 API 输入/输出
from pydantic import BaseModel, Field, field_validator
from decimal import Decimal
from typing import List

class CreateOrderRequest(BaseModel):
    customer_id: str
    items: List[dict]

    @field_validator('items')
    @classmethod
    def validate_items(cls, v):
        if not v:
            raise ValueError("订单项不能为空")
        return v

class OrderResponse(BaseModel):
    order_id: str
    status: str
    total: Decimal

    model_config = {"from_attributes": True}  # ✅ 支持从 dataclass 转换

六、脱离数据库驱动开发的实践路径

  • 领域模型优先:先设计聚合根与值对象,再选择 ORM 或事件存储,禁止从数据库表结构反推模型;
  • Pydantic 归位:仅用于 DTO、配置校验、序列化,不作为聚合根基类;
  • 行为封装:业务规则写在聚合根方法中,而非 Service 层或 Validator 中;
  • 值对象不可变:使用 frozen=True 或 dataclass(frozen=True),金额一律用 Decimal
  • 依赖倒置:仓储接口在领域层,实现在基础设施层,领域层零外部依赖;
  • 事件驱动解耦:跨上下文通信通过领域事件,而非直接调用其他上下文的 Service。

七、总结与建议

实践维度❌ 常见错误做法✅ 推荐做法
上下文划分按技术分层(models/services)按业务模块划分目录,每个上下文独立
聚合根建模Pydantic BaseModel + field_validatordataclass/普通类 + 工厂方法 + 行为封装
值对象封装mutable dict / floatfrozen dataclass/BaseModel + Decimal
业务规则声明式 validator / Service 层聚合根方法 + property 计算
领域事件内联未定义类 / 直接存库独立 frozen dataclass + 事件收集模式
Pydantic 定位领域模型基类DTO / 序列化 / 输入验证(边界层)
仓储接口混入服务层或基础设施层定义在领域层(Protocol/ABC),实现在基础设施层
金额类型floatDecimal(始终)

最终目标:将业务逻辑封装在领域模型中,通过类型系统与不可变性保障一致性,让 Pydantic 回归其擅长的边界验证与序列化职责,避免“数据库驱动开发”与“框架绑定过深”的双重陷阱,提升系统的领域表达能力与长期可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

创世宇图SHARE

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值