Redux和随之而来的架构观点将与领域驱动设计(DDD)的原则发生冲突。Redux的观点和DDD的原则之间的冲突,虽然如果DDD被优先考虑的话是可以解决的,但是如果你想让你的React前端架构与DDD保持一致,那么最好完全避免Redux。冲突的要点是:
- 保持域的纯洁性,不受给定技术的实现细节的影响
- 使用用例模式演示
- 使用Redux作为仓库
我不会在这篇文章中讨论为什么你想在React中做DDD。如果你认为在React中没有一个有效的理由去做DDD,那么这篇文章不适合你。我希望写一个关于React前端中的DDD何时是合适的,但这是一个值得自己讨论的话题。
我不会在这里展示代码示例。如果您想要示例,请随意阅读此存储库中的示例。我认识到应用DDD有很多方法,所以在这里提供具体的代码示例会适得其反。
纯粹的领域
业务逻辑的设计和实现构成了领域层。在模型驱动的设计中,领域层的软件构造反映了模型概念。当域逻辑与程序的其他关注点混合时,实现这种对应是不切实际的。隔离领域实现是领域驱动设计的先决条件。- 领域驱动设计,Eric Evans
上面的引用本质上说,特定技术的偶然复杂性不应该被允许在域核心中。
- 领域是状态如何被允许改变的唯一仲裁者。
- Redux建议在reducer中尽可能多地使用逻辑,这与上条直接冲突。
Redux建议在reducer中计算新状态的逻辑
尽可能将计算新状态的逻辑放在适当的reducer中,而不是放在准备和分派操作的代码中(如单击处理程序)。这有助于确保更多的实际应用逻辑易于测试,能够更有效地使用时间旅行调试,并有助于避免可能导致突变和错误的常见错误。
作者观点:
Reducer不应该被允许在域核心中,因为它们是Redux本身的构造。
换句话说,reducer是Redux的偶然复杂性。允许它们进入域核心是将域逻辑与Redux的实现细节混合在一起。纯域核心应该不知道状态如何持久化和检索的机制。
用例模式:像导演一样指挥服务“大合唱”
想象一下,你在拍一部电影,里面有很多演员(服务、领域实体、存储库),你得让这些演员按照剧本(业务逻辑)配合得天衣无缝。这就是用例(Use-Case)的角色!它就像个大导演,站在片场中央,喊着“灯光!摄像!开演!”来协调各种服务、数据和操作。
用例是“领域层”的一部分,啥叫领域层?就是你程序里最核心、最重要的逻辑部分。
用例只能往“核心”里靠,不能随便依赖外面的东西。就像导演只听剧本的,不能被道具组或灯光组牵着鼻子走。
用例需要一些“接口”(像剧本里的角色说明),这些接口得由外面的“消费者”或者“依赖注入机制”来填充具体的实现(比如找个真演员来演主角)。
而且,很多时候用例得异步工作,因为它可能要跟服务器“喊话”或者处理其他慢吞吞的操作。
在 Redux(一个管理数据状态的工具)里,Thunks 就像导演的助理,专门负责处理异步的“杂活”。比如,你需要从服务器抓点数据,或者在组件里处理复杂的逻辑,Thunks 就是官方推荐的干活方式。
但问题来了! 用例不应该直接依赖 Redux,因为这会让核心逻辑跟 Redux 绑得太死,就像导演非得用某个品牌的摄像机拍电影,换个牌子就拍不了了。这会让你的代码变得不灵活。
领域事件:像广播电台一样传递消息
在有些系统里(比如“最终一致性”系统),不同模块之间得像广播电台一样,通过领域事件来喊话:“嘿!库存变了!”或者“订单取消了!”其他模块听到后会做出反应。用例处理器(Use-Case Handler)就像收音机,专门监听这些广播,然后根据情况再导演一出新的“戏”。
在 Redux 里,这种监听可以用 RTK 监听中间件来实现。就像你有个智能助手,随时盯着 Redux 里发生的事(比如某个动作被触发),然后根据情况启动更复杂的操作,比如长时间的异步任务,或者像“后台线程”那样的工作。
但又有个问题! 这还是会让你的核心逻辑跟 Redux 耦合得太紧,就像收音机只能收一个台,换个台就得大改。
Redux 当“仓库”使:存东西的地方,但有点不听话
在 领域驱动设计(DDD) 里,仓库(Repository) 就像个超级图书馆,里面存了一堆东西(比如订单、用户数据),你可以按条件去查,比如“给我找所有北京的订单”。仓库的牛掰之处在于,它把跟数据库打交道的复杂操作藏起来了,你只管说“给我这个”,它就帮你搞定。
Redux 其实也可以当仓库用!它就像个内存里的“数据库”,存着你的程序状态(State)。用 Redux Toolkit(RTK) 里的 RTK-Query,还能帮你从服务器抓数据。Redux 的 Reducer(处理状态变化的函数)和 Action(触发变化的指令)就像仓库的管理员,负责存、删、查数据。比如:
- 存和删:通过 Action 实现,比如“保存订单”或“删除用户”。
- 查:可以用 useSelector 钩子,或者直接从 Redux 里掏数据(store.getState().someReducer)。
但这儿有个矛盾!
Redux 官方建议把 Action 当成“事件”(比如“订单已创建”),而不是“设置器”(比如“设置订单状态为完成”)。
为啥?因为“事件”听起来更有意义,动作日志也更清晰。而“设置器”会让 Action 种类太多,触发太频繁,日志乱七八糟。
在 DDD 里,仓库本来就不喜欢零碎的“设置”操作,而是更倾向于处理整个“聚合根”(比如一整个订单对象)。
所以 Redux 的建议跟 DDD 其实不完全冲突,只不过 Redux 喜欢用“过去式”描述事件(比如“订单已保存”),有点像咬文嚼字。
更大的问题是: Redux 没有“事务”支持!啥叫事务?就像你去银行转账,钱从 A 账户扣了,必须保证到 B 账户,否则就得回滚。
Redux 里如果你同时改两个仓库(比如订单和库存),得发两个 Action。如果中间出问题,可能导致数据不一致。
Redux 建议把多个操作塞到一个 Action 里,但这又会让领域逻辑漏到 Redux 里,破坏了仓库的“纯净”。
还有,Redux 的每次操作可能会触发界面多次刷新,超级费性能!虽然可以用“动作批处理”插件来减少刷新,但这又让代码跟 Redux 绑得更紧,DDD 的“模块化”理想就更难实现了。
Redux 的“意见”太低级,DDD 嫌它管得太细
Redux 就像个爱指手画脚的“技术宅”,它的建议(比如“动作要像事件”“别发太多动作”)都围绕着自己的技术细节。而 DDD 是个更高层次的“哲学家”,它不在乎你用啥工具(Redux、Zustand 还是别的),只关心你的代码逻辑能不能清晰地表达业务。
Redux 的“意见”太多太细,感觉像在教你怎么用它的“玩具”玩游戏,而不是帮你搭个大舞台。相比之下,Zustand 或 Tanstack Query 就低调多了,像个老实的手艺人,干活不挑三拣四,适合跟 DDD 配合。
Redux 也有它的好,比如 RTK、RTK-Query 让代码写起来更爽,还有持久化工具、时间旅行调试等“花活”。但如果按 DDD 的思路,Redux 最好只用来做“仓库”或者“网络服务”,别掺和核心逻辑。可惜,Redux 总想当主角,硬要跟你程序的核心逻辑“谈恋爱”,这就让 DDD 的粉丝很头疼。
总结:Redux 跟 DDD 能凑合,但得小心
用 Redux 做 DDD 就像请了个大牌导演来拍独立电影——能干活,但得防着它抢戏。Redux 的 API 挺好用,但它的“意见”太多,容易跟 DDD 的理念打架。如果你想用 DDD 又想用 Redux,得时刻提醒自己:别让 Redux 爬到核心逻辑里去!不然,你得跟 Redux 的“粉丝”吵来吵去,累得慌。