HelloWorld 领域模型教程

用HelloWorld示例建领域模型,其实就是把业务用语翻译成代码里的概念:先把需求拆成实体、值对象、聚合根与领域服务,再用不变量和用例约束行为,通过测试驱动逐步实现并用接口适配器隔离技术细节。这样模型既能表达业务意图,又方便演进与复用,最终让代码成为能说话的业务文档。

HelloWorld 领域模型教程

先说明为什么要用领域模型

领域模型并不是为了炫架构,而是为了解决一个常见问题:当业务复杂、团队增加、系统演进时,代码变成了难以理解的杂物堆。领域模型帮你把业务语言映射到软件结构,让团队用同一套概念讨论问题(也就是“通用语言”)。想像一下,大家都能读懂代码里的实体名称和方法,那沟通就少了很多误解。

用费曼法则来理解

把领域模型看作教别人一件事情:用最简单的词解释实体和行为,然后展示例子,最后把技术细节藏在“接口”后面。当你能用一句话把一个聚合介绍清楚,说明你真正理解了它。

核心概念一览(先搞清楚词)

  • 实体(Entity):有身份标识、会变化的对象,例如用户、订单。
  • 值对象(Value Object):无身份,仅表示属性集合,例如金额、地址,通常是不可变的。
  • 聚合与聚合根(Aggregate / Aggregate Root):一组相关对象的边界,聚合根负责不变量的一致性。
  • 领域服务(Domain Service):当某个操作不自然属于某个实体或值对象时放在这里,专注于业务逻辑。
  • 仓储(Repository):持久化抽象,让领域模型不直接依赖数据库。
  • 领域事件(Domain Event):重要业务事实发生的记录,用于解耦与异步协作。

表格对照一下(方便记忆)

概念 职责 示例
实体 代表可变的业务对象并持有身份 用户(User)、订单(Order)
值对象 封装属性集合,通常不可变 地址(Address)、金额(Money)
聚合根 维护聚合边界与不变量 订单(Order)作为订单聚合根
领域服务 实现跨实体的业务规则 支付服务(PaymentService)

用HelloWorld举例来一步步建模型

我们用一个非常简单的业务场景:系统需要对外提供问候语(Greeting),并记录每次问候的语言与发起者。看上去很小,但足够展示如何划分实体、值对象、聚合与服务。

步骤 1:识别用例与边界

  • 用例 A:客户端请求生成问候语(根据语言、格式等)。
  • 用例 B:记录问候历史(谁、何时、语言、内容)。
  • 边界:问候生成是核心业务;持久化、外部翻译服务是边界问题。

步骤 2:提取领域概念

从用例里抽出名词:问候(Greeting)、用户(User)、语言(Language)、历史记录(GreetingRecord)。然后判断哪些是实体,哪些是值对象。

  • 实体:User(有ID,会变化)、GreetingRecord(有ID和时间戳)
  • 值对象:Language(code与名称)、GreetingText(文本和格式)
  • 聚合根:GreetingAggregate(包含GreetingText与生成行为)或将GreetingRecord作为独立聚合

步骤 3:定义不变量与行为

比如不允许生成空问候;某些语言需要特定模板。把这些规则放在聚合根或领域服务里,保证在内存中就能被校验。

步骤 4:用伪代码描述模型(不是实现细节)

伪代码可以帮助大家快速达成一致(注意这里不是语言依赖的实现):

// GreetingAggregate

class GreetingAggregate {

id

generateGreeting(user, language) {

validate(user, language)

text = templateFor(language).apply(user.name)

emit GreetingGeneratedEvent(…)

return new GreetingText(text)

}

}

测试驱动与演进(TDD 的力量)

从一个简单的测试用例开始:当请求英语问候时,返回 “Hello, !”。先写测试,再实现最小模型,让测试通过。随后引入更多场景(多语言、特殊字符、模板替换、外部翻译失败时的回退策略)。每新增场景,都伴随一个或多个测试,这样演进时不怕回归。

测试粒度建议

  • 单元测试:聚合和领域服务的不变量与行为。
  • 集成测试:仓储与接口适配器是否配合领域模型正常工作。
  • 契约测试(可选):与外部翻译服务或消息总线的交互契约。

实现细节与架构映射(怎么把模型变成代码)

常见做法是采用六边形架构(Hexagonal)或洋葱架构(Onion):把领域模型放在中心,外围是接口(Repository、Service API),再外层是实现(数据库、HTTP、消息队列)。这样当技术栈变更时,领域代码几乎不动。

接口示例

  • GreetingRepository:保存GreetingRecord、查询历史。
  • TranslationGateway(可选):调用外部翻译服务。

领域事件与异步处理

当问候生成后,你可能想异步发送统计、触发通知或写入分析系统。把这些作为领域事件(GreetingGenerated),领域只负责发布事件,具体消费者在外层实现。这样既解耦又便于扩展。

常见坑与实践建议(说点生活化的)

  • 不要把一切逻辑都塞进实体构造函数里,那样看起来“面向对象”但很难测试。
  • 避免把仓储当成简单的ORM代理,仓储应该返回聚合或列表而不是原始数据库记录。
  • 值对象要不可变,能帮你减少很多同步和并发问题。
  • 别急着抽象。先写几个具体的实现,让用例稳定后再抽象出接口。

团队协作建议

频繁的领域语言对齐会议十分重要。把业务术语写成文档(Ubiquitous Language),在代码里复用这些术语(类名、方法名、异常名),这样需求变更时大家能同步理解。

演化策略:当模型需要变更时怎么做

演化时要分两步走:先扩展(非破坏性),再迁移(必要时)。例如新增问候模板字段,先在聚合里默认处理老数据,然后逐步迁移历史数据;如果要拆聚合,先做并行写入与回读策略,确保平滑切换。

版本兼容小技巧

  • 事件采用可扩展的schema(新增字段可选),消费者向后兼容。
  • 接口适配器实现向下兼容,给老客户端保留适配层。

工具与书籍推荐(方便查阅)

  • 《领域驱动设计》——Eric Evans
  • 《实现领域驱动设计》——Vaughn Vernon
  • 实践中常用语言的测试框架和反腐层库(按你团队栈选择)。

嗯,说了这么多,回到最开始那点:领域模型最有价值的地方是把“谁在做什么”这件事清楚地写入代码中。其实建模并不复杂,关键是多和业务方沟通、从小处验证、逐步演进。需要的话我们可以把这个 HelloWorld 模型拆成具体的仓储接口、事件结构和测试用例,慢慢把它敲成可运行的样子。