为什么 DDD 和 Kotlin 是理想组合
当项目不断扩展,业务需求开始变得像一团乱麻时,普通的 CRUD 方法就不再适用了。你会发现自己一半的代码都是无穷无尽的 if-else 检查,而逻辑上的任何改动都会导致十几个文件需要修改。熟悉吗?
这时,领域驱动设计(DDD) 登场了——这是一种将业务逻辑置于核心地位的方法。而 Kotlin 凭借其简洁的语法、强大的类型系统以及对函数式概念的支持,成为实现 DDD 的理想工具。在本文中,我们将探讨如何将这两者结合起来,让你的代码不仅可用,而且 富有表现力 且 灵活。
如果你正在使用 Kotlin 编写代码,并希望你的架构充满活力而不是吱吱作响——那么你来对地方了。今天,我们不仅会讨论理论,还会编写真实的代码示例。
领域驱动设计:为忙碌者准备的快速入门
在深入代码之前,让我们先统一一下术语。DDD 不是一个框架,也不是一个库。它是一套原则,帮助开发人员和业务人员使用同一种语言交流。
关键概念:
- 通用语言(Ubiquitous Language) —— 代码中的术语应与业务分析师和客户使用的术语一致。
- 限界上下文(Bounded Context) —— 一个清晰的边界,在该边界内模型才有意义。例如,销售上下文中的“客户”和支持上下文中的“客户”是不同的实体。
- 实体(Entity) —— 具有唯一标识符且随时间变化的对象。
- 值对象(Value Object) —— 一个不可变对象,由其属性而非标识符定义。
- 聚合(Aggregate) —— 一组相关对象,被视为一个整体。
- 仓储(Repository) —— 用于访问数据的抽象,隐藏了存储细节。
听起来很抽象?现在让我们用 Kotlin 来实现它。
Kotlin 中的值对象:优雅与安全
我们从最令人愉快的部分开始——值对象(VO)。在 Kotlin 中,由于 data class 的存在,实现它们是一种享受。
假设我们正在一个酒店预订系统中工作。我们有一个“入住期间”的概念。这不是一个实体——它只是一组日期。是值对象的理想候选。
data class StayPeriod( val checkIn: LocalDate, val checkOut: LocalDate) { init { require(checkIn.isBefore(checkOut)) { "入住日期必须早于退房日期" } }
val totalNights: Int get() = ChronoUnit.DAYS.between(checkIn, checkOut).toInt()
fun overlapsWith(other: StayPeriod): Boolean { return !(this.checkOut.isBefore(other.checkIn) || this.checkIn.isAfter(other.checkOut)) }}为什么这很酷?
- 不可变性 —— 所有字段都是
val。没有人会意外更改入住日期。 - 内置验证 ——
init块确保对象仅在有效状态下创建。 - 行为 ——
overlapsWith方法封装了业务规则。你不需要在服务中查找日期重叠,而是直接询问对象本身。
在 DDD 中,我们称之为 富模型 —— 对象不仅存储数据,还能够执行业务操作。
实体:当身份很重要时
现在我们来谈谈 实体。与值对象不同,实体是可变的并且具有身份。在 Kotlin 中,我们使用普通类,但重点放在标识符上。
示例——酒店预订(Booking):
class Booking( val id: BookingId, val roomNumber: Ro