为什么CQRS和事件溯源不是“精英专属”?
\n\n当我们听到“CQRS”或“事件溯源”这些术语时,许多JavaScript开发者会联想到Java或C#企业级架构中的某些东西。复杂的方案、无穷无尽的事件、奇怪的存储……但如果我告诉你,这些模式可以(并且应该!)在普通的Node.js应用程序中使用呢?
\n\n在本文中,我们将探讨CQRS和事件溯源如何帮助解决实际问题:数据冲突、复杂日志记录、扩展和调试。而且这一切——都基于纯JavaScript,无需魔法或复杂的框架。
\n\n什么是CQRS(命令查询职责分离)?
\n\n想象一下,你的应用程序就像一家餐厅。服务员(查询)和厨师(命令)在同一个空间工作。迟早他们会互相干扰。CQRS建议将这些区域分开:服务员在大厅工作(读取),而厨师在厨房工作(写入)。
\n\nCQRS 是一种将操作分为两个通道的原则:
\n- \n
- 命令(Commands) — 改变系统状态(创建、更新、删除) \n
- 查询(Queries) — 仅读取数据(读取) \n
在经典的REST API中,我们通常使用同一个对象进行读写。例如,UserController包含方法GET /users/:id和POST /users。CQRS建议将它们分散到不同的层中。
JavaScript中CQRS的简单示例
\n\n// 命令(改变状态)\nclass CreateUserCommand {\n constructor(name, email) {\n this.name = name;\n this.email = email;\n }\n}\n\nclass UserCommandHandler {\n async handle(command) {\n // 验证、业务逻辑、保存\n return await db.users.insert({\n name: command.name,\n email: command.email,\n created_at: new Date()\n });\n }\n}\n\n// 查询(仅读取)\nclass GetUserQuery {\n constructor(userId) {\n this.userId = userId;\n }\n}\n\nclass UserQueryHandler {\n async handle(query) {\n // 仅读取,不做任何更改!\n return await db.users.findOne({ _id: query.userId });\n }\n}\n\n这看起来像是在让代码变得更复杂?是的,乍一看确实如此。但当你的应用程序增长到50多个端点时,命令和查询的分离将帮助你避免:
\n- \n
- 在GET请求中意外修改数据 \n
- 缓存问题(查询易于缓存,命令则不然) \n
- 多名开发者并行工作时产生的冲突 \n
事件溯源:记住每一个操作
\n\n现在想象一下,与其存储用户的当前状态,不如存储所有发生在他们身上的事件。不是“伊万,25岁,莫斯科”,而是一个序列:
\n- \n
- “用户已创建:伊万” \n
- “用户将年龄改为25” \n
- “用户搬到了莫斯科” \n
事件溯源 是一种模式,其中每个状态更改都作为不可变事件保存。当前状态通过“重放”所有事件来计算。
\n\n在JavaScript中实现事件存储
\n\nclass EventStore {\n constructor() {\n this.events = [];\n }\n\n append(aggregateId, event) {\n this.events.push({\n aggregateId,\n event,\n timestamp: new Date()\n });\n }\n\n getEvents(aggregateId) {\n return this.events.filter(e => e.aggregateId === aggregateId);\n }\n}\n\n// 从事件重建状态\nfunction rebuildState(events) {\n return events.reduce((state, event) => {\n switch(event.event.type) {\n case 'USER_CREATED':\n return { ...state, name: event.event.name, age: 0 };\n case