为什么 CQRS 和事件溯源不仅仅是流行词汇
\n\n想象一下,你的代码是一座巨大的博物馆。通常我们只保存展品的最后一个版本:“订单当前的样子”。但如果我们想知道订单如何变成这个状态呢?是谁按下了“发送”按钮?状态是什么时候改变的?正是在这里,CQRS(命令查询职责分离)和事件溯源在Kotlin中登场了。
\n\n在本指南中,我们不会只是从维基百科复制理论。我们将分析真实的示例,编写代码,并理解这些模式如何将 CRUD 操作的混乱转变为清晰的架构。如果你厌倦了“意大利面条式代码”,并希望构建易于扩展和调试的系统——那么你来对地方了。
\n\n用简单的话解释什么是 CQRS
\n\n在标准架构中,我们使用相同的模型来读写数据。这就像你用同一把勺子喝汤并同时搅拌它。CQRS 建议分离职责:
\n\n- \n
- Command(命令)——改变状态。写入请求(创建、更新、删除)。 \n
- Query(查询)——读取状态。仅限 SELECT 操作。 \n
为什么需要这样做?想象一个在线商店。当用户下订单时,我们必须检查库存、扣减商品、在数据库中创建记录、发送电子邮件。这是一个复杂的命令。而当客户只是查看他们的订单列表时——这是一个简单的查询。将它们混合在同一个方法中——是通往 bug 的捷径。
\n\n在 Kotlin 中使用 CQRS 的优势
\n- \n
- 性能:可以以不同方式优化读写(不同的数据库、缓存、索引)。 \n
- 安全性:分离访问权限(管理员写入,用户读取)。 \n
- 可扩展性:命令和查询可以分布到不同的服务器上。 \n
事件溯源:保存历史,而非事实
\n\n如果说 CQRS 分离了操作,那么事件溯源则改变了数据存储的方式。我们不是存储当前状态(例如,“余额:1000 卢布”),而是存储一系列事件:“存入 500”,“消费 200”,“扣除手续费 50”。
\n\n当前状态只是按顺序应用所有事件的结果。这赋予了我们超能力:我们可以“回放”时间,恢复到任何日期的状态,或者分析用户行为。
\n\n现实生活中的示例:银行账户
\n// 事件\ndata class MoneyDeposited(val amount: BigDecimal, val timestamp: Instant)\ndata class MoneyWithdrawn(val amount: BigDecimal, val timestamp: Instant)\n\n// 聚合(实体)\nclass BankAccount {\n private var balance: BigDecimal = BigDecimal.ZERO\n private val events = mutableListOf<Any>()\n\n fun deposit(amount: BigDecimal) {\n if (amount <= BigDecimal.ZERO) throw IllegalArgumentException("金额必须为正数")\n val event = MoneyDeposited(amount, Instant.now())\n apply(event)\n events.add(event)\n }\n\n fun withdraw(amount: BigDecimal) {\n if (amount > balance) throw IllegalStateException("余额不足")\n val event = MoneyWithdrawn(amount, Instant.now())\n apply(event)\n events.add(event)\n }\n\n private fun apply(event: Any) {\n when (event) {\n is MoneyDeposited -> balance += event.amount\n is MoneyWithdrawn -> balance -= event.amount\n }\n }\n\n fun getBalance(): BigDecimal = balance\n fun getUncommittedEvents(): List<Any> = events.toList()\n}\n\nCQRS 和事件溯源如何协同工作
\n\n这是经典的组合。CQRS 负责分离通道,而事件溯源 负责存储