ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

或许是领域建模的真相

或许是领域建模的真相 或许是领域建模的真相我见过太多团队在领域建模上栽跟头。他们捧着 DDD 的蓝皮书画着四层架构图最后却写出一堆贫血模型和上帝服务。问题出在哪或许我们从来就没理解领域建模的真正含义——它不是从业务流程图里“推导”出来的而是从代码的约束中“生长”出来的。### 真相一领域模型是约束的产物不是现实的映射大多数开发者以为领域模型是业务现实的“镜像”于是试图用Customer、Order、Product这些名词去“复刻”业务。但真正的领域模型回答的是“在这个系统里什么操作是合法的”而不是“现实世界长什么样”。看一个反例。假设我们有一个订单系统业务规则是“订单一旦支付就不能修改收货地址”。很多人的第一版模型长这样python# 反例贫血模型 无约束class Order: def __init__(self, order_id, address, paidFalse): self.order_id order_id self.address address # 公共字段随意修改 self.paid paid def mark_paid(self): self.paid True# 任何代码都能直接改 address包括已支付的订单order Order(A001, 北京)order.mark_paid()order.address 上海 # 系统允许了但业务不允许这个模型没有“约束”住非法状态。真正的领域建模要把业务规则变成代码层面的“不可绕过”的束缚python# 正例用状态机 方法封装约束class Order: def __init__(self, order_id, address): self.order_id order_id self._address address self._paid False # 私有状态外部不可直接修改 def pay(self): # 支付后锁定地址 self._paid True def change_address(self, new_address): # 约束已支付订单不可修改地址 if self._paid: raise PermissionError(已支付订单不可修改地址) self._address new_address property def address(self): return self._address# 现在非法操作在编译期/运行期就被拦截了order Order(A001, 北京)order.pay()try: order.change_address(上海)except PermissionError as e: print(业务规则生效:, e)关键点领域模型的价值不在于“描述现实”而在于“拒绝非法状态”。你写出的每一个if判断、每一个私有字段、每一个抛出的异常都是模型的核心部分。### 真相二持久化模型 ≠ 领域模型很多团队直接用 ORM 的实体类当作领域模型这是最大的误区。持久化模型关心的是“怎么存”领域模型关心的是“怎么用”。两者经常冲突——比如订单状态数据库里可能存一个int但领域里需要的是枚举状态机。我们看一个实战例子用 Python 的dataclass和 SQLAlchemy 演示分离python# 领域模型纯内存不依赖任何 ORMfrom dataclasses import dataclassfrom enum import Enum, autoclass OrderStatus(Enum): DRAFT auto() PAID auto() SHIPPED auto()dataclassclass OrderDomain: order_id: str total: float status: OrderStatus def ship(self): if self.status ! OrderStatus.PAID: raise ValueError(只有已支付订单才能发货) self.status OrderStatus.SHIPPED# 持久化模型负责映射数据库from sqlalchemy import Column, String, Float, Integer, create_enginefrom sqlalchemy.ext.declarative import declarative_basefrom sqlalchemy.orm import sessionmakerBase declarative_base()class OrderORM(Base): __tablename__ orders id Column(String, primary_keyTrue) total Column(Float) status_int Column(Integer) # 数据库存 int# 手动转换领域模型 - 持久化模型def to_domain(orm_obj: OrderORM) - OrderDomain: return OrderDomain( order_idorm_obj.id, totalorm_obj.total, statusOrderStatus(orm_obj.status_int) )def to_orm(domain_obj: OrderDomain) - OrderORM: return OrderORM( iddomain_obj.order_id, totaldomain_obj.total, status_intdomain_obj.status.value )# 使用示例engine create_engine(sqlite:///:memory:)Base.metadata.create_all(engine)Session sessionmaker(bindengine)session Session()domain OrderDomain(A002, 99.9, OrderStatus.PAID)orm_obj to_orm(domain)session.add(orm_obj)session.commit()# 从数据库读回转成领域模型loaded_orm session.query(OrderORM).filter_by(idA002).one()loaded_domain to_domain(loaded_orm)loaded_domain.ship() # 可以安全调用业务方法print(f订单状态: {loaded_domain.status})为什么必须分离因为 ORM 的模型有生命周期管理session、懒加载、脏检查这些技术细节会污染业务逻辑。领域模型应该是“纯函数式”的不依赖外部环境。一旦你把 ORM 实体当领域模型你就会在业务代码里看到session.commit()这种鬼东西。### 真相三聚合边界是性能与一致性的平衡点领域建模最难的决策是“聚合怎么划分”。很多人按直觉把“订单”和“订单项”放在一个聚合里结果每次操作都要加载整个聚合性能崩溃。真相是聚合边界取决于事务一致性的需求而不是对象关系的亲疏。实战例子电商系统中订单和订单项。如果业务规则是“修改订单项数量时必须同时更新订单总额”那它们必须在一个聚合里。但如果“订单项”可以被独立修改比如售后只改某个商品那就该拆开。python# 拆分布局订单聚合 订单项独立聚合使用事件最终一致性from dataclasses import dataclass, fieldfrom typing import Listdataclassclass OrderItem: item_id: str product_name: str quantity: int price: float def increase_quantity(self, delta: int): if delta 0: raise ValueError(增量必须为正数) self.quantity deltadataclassclass OrderAggregate: order_id: str items: List[OrderItem] field(default_factorylist) total_amount: float 0.0 def add_item(self, item: OrderItem): self.items.append(item) self.total_amount item.price * item.quantity def get_item(self, item_id: str) - OrderItem: for item in self.items: if item.item_id item_id: return item raise KeyError(f未找到商品 {item_id})# 如果订单项独立更新可以使用领域事件来同步总额dataclassclass ItemQuantityChanged: order_id: str item_id: str old_qty: int new_qty: int# 事件处理器监听事件重新计算总额def on_quantity_changed(event: ItemQuantityChanged, order: OrderAggregate): item order.get_item(event.item_id) delta event.new_qty - event.old_qty order.total_amount delta * item.price print(f总额已更新为: {order.total_amount})# 使用示例order OrderAggregate(O001)order.add_item(OrderItem(P1, 鼠标, 2, 50.0))order.add_item(OrderItem(P2, 键盘, 1, 200.0))item order.get_item(P1)old_qty item.quantityitem.increase_quantity(3) # 修改数量# 发布事件实际可用消息队列on_quantity_changed( ItemQuantityChanged(order.order_id, P1, old_qty, item.quantity), order)print(f聚合总额: {order.total_amount})实战教训我曾经把一个订单聚合拆成 5 个独立聚合用事件驱动同步数据。性能提升了一个数量级但代码复杂度也上去了。没有银弹——聚合边界就是一场权衡一致性要求高就聚合大性能要求高就聚合小。### 真相四领域建模是持续重构不是一次性设计很多团队花两周时间画 UML 图然后进入“编码实现”阶段。这是错误的。领域模型应该伴随代码的每一次重构而进化。当你发现一个方法经常需要传 3 个参数或者一个类频繁被if-else分支那就是在告诉你“模型边界错了”。我给出一个反模式重构实例一开始把所有金额计算都放在Order类里后来发现折扣规则变化频繁于是抽离出策略模式python# 重构前Order 类承担所有计算class Order: def __init__(self, items, discount_rate0.0): self.items items self.discount_rate discount_rate def total(self): raw_total sum(item.price * item.qty for item in self.items) return raw_total * (1 - self.discount_rate)# 重构后策略模式让领域模型更专注于“是什么”from abc import ABC, abstractmethodclass DiscountStrategy(ABC): abstractmethod def apply(self, raw_total: float) - float: passclass NoDiscount(DiscountStrategy): def apply(self, raw_total: float) - float: return raw_totalclass PercentageDiscount(DiscountStrategy): def __init__(self, rate: float): self.rate rate def apply(self, raw_total: float) - float: return raw_total * (1 - self.rate)class Order: def __init__(self, items, discount: DiscountStrategy): self.items items self.discount discount def total(self): raw_total sum(item.price * item.qty for item in self.items) return self.discount.apply(raw_total)# 使用策略可灵活组合order Order(items[], discountPercentageDiscount(0.2))重构的触发信号当你在代码里看到if type A或者类名里有“Manager”“Util”时就是领域模型在喊救命。### 总结领域建模的真相不是“画图”而是用代码表达约束。它要求你1. 把业务规则变成不可绕过的代码约束私有字段、状态机、异常2. 将领域模型与持久化模型彻底分离保持业务代码纯净3. 用聚合边界平衡一致性与性能用事件解决跨聚合的最终一致性4. 把建模当成持续重构的过程而不是一次性的设计产物最后我建议你忘掉那些华丽的术语。去读你的代码找到那些“谁都能改”的公共字段找到那些“什么都能干”的上帝服务然后动手把它们拆掉、约束住。那才是领域建模真正开始的地方。
返回列表