行业资讯

拨开迷雾,找回自我:DDD 应对具体业务场景,Domain Model 到底如何设计?

发布时间:2026/7/26 18:52:18
拨开迷雾,找回自我:DDD 应对具体业务场景,Domain Model 到底如何设计? 拨开迷雾找回自我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自身负责借书检查而不是交给 Servicepython# 正确的 Domain Model 设计Python 示例from enum import Enumfrom dataclasses import dataclassclass MembershipLevel(Enum): NORMAL 1 ADVANCED 2 VIP 3dataclassclass 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(levellevel, max_books_per_monthlimits[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### 步骤3Service 层只做“协调”现在 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# 测试代码运行前需要先安装 pytestimport pytestdef test_normal_user_borrow_limit(): membership Membership.create(MembershipLevel.NORMAL) user User(user_id1, membershipmembership) # 借第一本书 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_id2, membershipmembership) 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_id3, membershipmembership) 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 还有“自我”吗如果它只是一个数据容器那就赶紧把它“救活”吧