- 在领域驱动设计(DDD)中,“Smart UI”是一个常见的反模式(Anti-Pattern)。它指的是将大量的业务逻辑、验证规则和流程控制代码都放在用户界面(UI)层的一种设计方式。
- “Smart UI”反模式的特征:
- 大量的业务逻辑在UI层:例如,一个按钮的点击事件处理程序中包含了复杂的业务规则判断、计算和数据持久化操作。
- 贫血的领域模型:领域对象(如
Order
、Customer
等)只有 getter
和 setter
方法,没有任何封装业务行为的方法。例如,一个 Order
对象自己不知道如何计算总价,而是由UI层的代码来获取其所有订单项,然后进行累加计算。
- 过程式而非面向对象:整个系统的设计倾向于面向过程的脚本式编程,而不是通过对象之间的协作来完成业务功能。
- 服务层薄弱:应用服务层(Application Service)可能变得非常薄,仅仅充当了从 UI 层接收数据,然后直接操作数据库/读取接口请求的角色,或者干脆不存在。
- 为什么“Smart UI”是反模式?
在DDD的核心思想中,领域模型应该是软件的核心,它应该包含丰富、内聚的业务逻辑和行为。而“Smart UI”恰恰违背了这一原则,带来了诸多问题:
- 业务逻辑重复:如果应用有多个客户端(如Web界面、移动App、API接口),那么相同的业务逻辑需要在每个UI中都实现一遍,导致大量的代码重复和不一致性。
- 难以维护和测试:由于业务逻辑和 UI 代码紧密耦合,使得单元测试变得非常困难。从测试四象限的角度来说,由于所有的业务规则都写在 UI 组件的事件处理器内部。我们只能通过在渲染 UI 并模拟用户点击的方式,通过断言 UI 上的文本/状体是否发生了变化来进行测试。
这其实是把 Q2象限的集成测试,当作 Q1 单元测试用。原本由 Q1 覆盖的的各种业务逻辑分支、边界条件,现在都必须通过大而笨重的集成测试进行覆盖。这种测试方式,不仅执行慢,而且难以定位是”UI 渲染问题”,是 “前端的业务逻辑计算错误”,还是”后端的 API 问题”。最终,测试金字塔变成了“测试冰淇淋筒”,顶层又大又重,底层空空如也,整体结构非常不稳定。
- 领域知识的丢失:核心的业务知识被分散在各个 UI 界面和事件处理器中,没有一个统一、内聚的地方来表达领域模型,使得新加入的开发者很难理解系统的核心业务。
比如当某一天新增加了埋点功能,可能会涉及到”侵入式埋点的设计”。Smart UI 中,在Smart UI中,开发者和产品经理的视角会不自觉地从领域事件降级为UI事件。
- 原始需求:“我们想知道‘提交订单’这个业务动作的成功率和失败原因。” (领域视角)
- Smart UI下的实现:“在‘提交订单按钮’的
onClick
事件里,判断一下如果成功就调用 track('submit_button_clicked_success')
,如果失败就调用 track('submit_button_clicked_failed')
。” (UI 视角)
submit_button_clicked
完全丢失了业务含义。它只描述了一个技术动作。如果未来系统交互发生变化,把按钮换成了一个链接,或者通过拖拽操作也能提交订单,那么这个埋点就完全失效或需要重做了。而“订单已提交”这个领域事件,无论UI如何变化,它本身的业务含义是永恒不变的。
- 灵活性差:当需要更换 UI 技术栈或者新增一种用户界面时,由于大量的业务逻辑被锁定在旧的 UI 中,迁移和重构的成本会非常高。这也导致不仅技术会纠结应该用什么框架,而且对微前端的引入带来了很大的麻烦。
- DDD所倡导的模式(与“Smart UI”相对):
与“Smart UI”相反,DDD提倡 Smart domain 和 Dumb UI/Thin UI 的组合:
- 丰富的领域模型:领域对象不仅包含数据,还封装了与之相关的业务行为和规则。例如,
Order
对象自己应该有一个 CalculateTotalPrice()
的方法。
- 哑UI/瘦UI:UI层应该尽可能地“笨拙”,它只负责:
- 显示领域模型的状态。
- 将用户的操作意图(如“提交订单”)传递给应用服务层。
- 它不应该包含任何核心的业务逻辑。