拨开迷雾,找回自我:DDD 应对具体业务场景,Domain Model 到底如何设计?
作为一名在软件行业摸爬滚打多年的博主,我经常听到这样的困惑:“DDD(领域驱动设计)听起来很高大上,但实际用起来却感觉像在迷雾中摸索——到底该怎么设计 Domain Model?” 别急,今天我们就用通俗易懂的语言,结合具体业务场景,一步步拨开迷雾,找回设计 Domain Model 的“自我”。## 什么是 Domain Model?为什么它如此重要?简单来说,Domain Model是业务领域的核心“骨架”,它用代码抽象出业务中的概念、规则和关系。比如在电商系统中,Order、Product、Customer就是模型;在银行系统中,Account、Transaction则是模型。好的 Domain Model 能直接反映业务逻辑,让代码和业务人员“说同一种语言”。但很多开发者容易陷入两个极端:-过度抽象:把模型设计得过于通用,导致业务规则被“稀释”。-贫血模型:只把模型当作数据容器(比如只有 getter/setter),业务逻辑全部散落在 Service 层。这两种做法都会让代码“失去自我”,变得难以维护。那么,正确的姿势是什么?我们通过一个具体业务场景来演示。## 业务场景:在线图书订阅系统假设我们要设计一个图书订阅系统,核心需求如下:1. 用户可以订阅不同等级的会员(普通、高级、VIP)。2. 每种会员等级有不同权限:普通会员每月可借 2 本书,高级会员可借 5 本,VIP 会员无限制。3. 用户借书时,系统必须检查是否超过当月限额,并更新剩余可借数量。4. 用户每月重置借书额度。初看似乎简单,但如果用“贫血模型”实现,代码会变成这样(反例):java// 反例:贫血模型public class User { private String id; private String membershipLevel; // "NORMAL", "ADVANCED", "VIP" private int booksBorrowedThisMonth; // getters and setters...}public class BorrowService { public void borrowBook(User user) { if (user.getMembershipLevel().equals("NORMAL") && user.getBooksBorrowedThisMonth() >= 2) { throw new RuntimeException("借书额度不足"); } // ... 类似逻辑 user.setBooksBorrowedThisMonth(user.getBooksBorrowedThisMonth() + 1); }}问题一目了然:业务规则(限额判断、额度更新)完全暴露在 Service 中,一旦规则变化(比如高级会员改为可借 10 本),需要修改多处代码。这就是“失去自我”的典型表现。## 正确的 Domain Model 设计:让模型“活”起来DDD 的核心思想是将业务逻辑封装在领域模型内部,让模型“自我管理”。下面我们一步步设计符合场景的 Domain Model。### 步骤1:识别核心实体和值对象-User:实体,有唯一标识(用户ID),包含会员等级和借书额度。-Membership:值对象(或枚举),定义不同等级的权限规则。-BorrowingRecord:值对象或实体,记录借书行为(可选,本文简化)。### 步骤2:将业务规则封装到模型中我们让User自身负责借书检查,而不是交给 Service:python# 正确的 Domain Model 设计(Python 示例)from enum import Enumfrom dataclasses import dataclassclass MembershipLevel(Enum): NORMAL = 1 ADVANCED = 2 VIP = 3@dataclassclass Membership: """值对象:会员等级及其权限规则""" level: MembershipLevel max_books_per_month: int # 每月最大借书数量(VIP 特殊处理为 -1 表示无限) @staticmethod def create(level: MembershipLevel) -> 'Membership': """工厂方法:根据等级创建对应权限""" limits = { MembershipLevel.NORMAL: 2, MembershipLevel.ADVANCED: 5, MembershipLevel.VIP: -1 # -1 表示无限 } return Membership(level=level, max_books_per_month=limits[level])class User: """实体:用户,包含借书额度管理""" def __init__(self, user_id: str, membership: Membership): self.user_id = user_id self.membership = membership self._books_borrowed_this_month = 0 # 本月已借数量 def can_borrow(self) -> bool: """检查是否还能借书""" if self.membership.max_books_per_month == -1: # VIP 无限 return True return self._books_borrowed_this_month < self.membership.max_books_per_month def borrow_book(self) -> None: """借书操作(包含业务规则)""" if not self.can_borrow(): # 这里可以抛出领域异常,而非通用 RuntimeException raise DomainException("本月借书额度已用完") self._books_borrowed_this_month += 1 # 实际项目中可能还需要记录借书历史,这里简化 def reset_monthly_quota(self) -> None: """每月重置额度(由定时任务触发)""" self._books_borrowed_this_month = 0# 领域异常class DomainException(Exception): pass### 步骤3:Service 层只做“协调”现在 Service 变得极其简洁,只负责调用模型的方法:pythonclass BorrowService: """应用服务:协调领域模型完成业务流程""" def __init__(self, user_repository): self.user_repository = user_repository def borrow_book(self, user_id: str) -> None: user = self.user_repository.find_by_id(user_id) if not user: raise ValueError("用户不存在") user.borrow_book() # 所有业务逻辑都在 User 内部 self.user_repository.save(user)### 步骤4:测试模型的行为我们可以轻松编写单元测试验证User的行为:python# 测试代码(运行前需要先安装 pytest)import pytestdef test_normal_user_borrow_limit(): membership = Membership.create(MembershipLevel.NORMAL) user = User(user_id="1", membership=membership) # 借第一本书 user.borrow_book() assert user._books_borrowed_this_month == 1 # 借第二本书 user.borrow_book() assert user._books_borrowed_this_month == 2 # 借第三本应该失败 with pytest.raises(DomainException): user.borrow_book()def test_vip_user_no_limit(): membership = Membership.create(MembershipLevel.VIP) user = User(user_id="2", membership=membership) for _ in range(100): # VIP 可以借任意多次 user.borrow_book() assert user._books_borrowed_this_month == 100def test_monthly_reset(): membership = Membership.create(MembershipLevel.NORMAL) user = User(user_id="3", membership=membership) user.borrow_book() user.reset_monthly_quota() assert user._books_borrowed_this_month == 0 user.borrow_book() # 重置后可以再借## 关键设计原则总结通过这个例子,我们可以提炼出 Domain Model 设计的核心原则:1.模型自治:业务规则(如限额检查)应该由模型自身维护,而不是放在 Service 中。2.显式建模:用Membership值对象封装等级规则,而不是用字符串"NORMAL"到处判断。3.防御性编程:在模型内部通过DomainException抛出业务异常,而不是返回错误码或直接抛通用异常。4.单一职责:User只负责用户相关行为,Membership只负责权限规则,BorrowService只负责协调。## 常见陷阱与应对-陷阱1:模型与数据库表一一对应。DDD 的模型是业务概念,不一定要和数据库表一致。比如Membership可以是值对象,序列化到User表中。-陷阱2:模型“膨胀”。如果User既要管借书,又要管支付、通知,就违反了单一职责。此时应拆分为Borrower、Payer等更细粒度的模型。-陷阱3:忽略领域事件。当借书成功后,可能需要触发“发送通知”等副作用。此时可以用领域事件(Domain Event)解耦,而不是直接写死。## 总结Domain Model 的设计并不是一场“高深莫测”的玄学,而是一场“拨开迷雾,找回自我”的旅程。核心在于:让模型承载业务逻辑,而不是让业务逻辑漂泊在 Service 中。通过本文的图书订阅示例,你应该已经看到,设计良好的 Domain Model 能带来:-可测试性:模型行为可以独立测试。-可维护性:业务规则集中管理,修改只影响局部。-可读性:代码直接反映业务语言。记住,DDD 不是银弹,但它能帮助你在复杂业务场景中保持清醒。下次当你面对一堆 CRUD 代码时,问问自己:我的 Domain Model 还有“自我”吗?如果它只是一个数据容器,那就赶紧把它“救活”吧!