如何构建长期运行的软件系统
Source: Dev.to
大多数软件系统的失败并不是因为技术选型错误。
它们之所以失败,是因为停止了学习。框架会老化,架构会时兴时衰,团队也会变动。致命的问题在于系统对业务的内部认知逐渐偏离现实——即使是微小的改动也会变得风险大、成本高且不可预测。
为什么系统会失败
当领域模型不再反映业务时,人们会提出重写、采用新架构或新框架,但根本问题仍然是技术无关的。一个长期存活的系统并不是保持不变,而是能够持续、增量地演进而不导致自身不稳定。
长期存活系统的特征
- 新功能通常可以在局部添加。
- 引入新特性时,已有行为很少会被破坏。
- 开发者用 领域术语 而不是技术术语来思考系统。
- 即使经过多年演进,也不需要大规模重写。
- 代码库反映了业务的真实运作方式,并“说”业务语言。
这些属性只有在概念模型与业务同步演进时才会出现。当演进停止,概念张力累积,职责不再清晰对齐,增量改动变得不可能。
用户故事作为领域验证
用户故事是对系统应支持的功能切片的平面描述——它捕捉 要做什么,而不是 内部职责应如何组织。把故事仅当作工作单来处理会侵蚀领域模型。
实现前需要问的问题
- 这个故事如何契合当前的领域模型?
- 哪些已有对象(按其原始定义)应参与其中?
- 这个故事是否揭示了缺失的概念或职责?
- 现有的职责边界仍然成立吗?
- 如果模型能够自然地容纳该故事,实施就很直接。
- 如果不能,摩擦本身就是 建模信号,而不是实现问题。跳过这一步会把职责决策转移到服务、工作流和编排代码中,悄然削弱模型。随着时间推移,领域模型不再主导行为,退化为被动的数据结构。
“模型优先”原则
“模型优先”并不意味着事先设计好所有东西或脱离现实抽象。它的含义是:
- 领域模型 是首要产物。
- 实现是为模型服务的。
- 职责边界驱动代码结构。
- 技术便利永远不能凌驾于概念正确性之上。
框架、库和架构应随时间可互换;而准确反映业务的领域模型则不应被替换。
贫血领域模型的危险
贫血领域模型常被描述为 “没有逻辑的实体”,但这一定义不完整。领域模型是 职责的结构;状态是职责的结果,而非定义本身。当职责被转移到代码的其他部分时,就会出现贫血模型。
即使对象中有逻辑,也可能出现贫血模型,若满足以下任意情况:
- 决策在拥有相关职责的对象 之外 做出,无论状态如何。
- 逻辑在定义概念边界的对象 之外 实现,即使这些对象在模型中存在。
- 不变式通过过程(工作流或服务)而非结构(模型)强制执行。
- 领域对象沦为仅仅的协调工具,真正的行为被推到外部编排器中。
示例:领域交互对象
领域交互对象可能定义:
- 何时以及如何进入领域。
- 适用的“一致性”或事务范围。
- 交互期间必须保持的哪些不变式。
它可能几乎没有持久化状态,却拥有关键职责。如果事务管理或一致性边界在该交互 之外 引入(例如在服务层或基础设施层),对象即使仍然存在,也失去了意义。
框架影响——Spring Boot 示例
框架对系统形态施加强大的引力,尤其在企业环境中。选择 Spring Boot,就等于让团队:
- 每年进行一次框架升级。
- 承担持续的许可证或支持费用。
这种被迫的 churn 会消耗工程师的注意力,即使业务领域本身没有变化。即便在稳定的领域,团队也必须不断:
- 适配框架的演进。
- 处理废弃特性。
- 重新验证基础设施关注点。
Spring 的默认配置和生态系统强烈倾向于 过程化、服务中心的设计:
- 无状态服务。
- 依赖注入的编排。
- 事务工作流。
- 被动的领域对象。
结果是职责碎片化,模型贫血。
事务管理作为领域关注点
事务常被视为纯技术关注点——只需在服务层配置或标注即可。实际上,事务定义了 业务层面的“一致性边界”,并回答如下问题:
- 什么构成有意义的工作单元?
- 哪些变更必须一起成功或一起回滚?
- 何时可以观察到中间状态?
- “全有或全无”在该领域到底意味着什么?
当事务边界在定义领域交互的对象 之外 引入时,模型失去了核心职责的所有权。系统可能在技术上是正确的,但在概念上变得不稳定。
持续建模提升交付速度
人们常误以为持续建模会拖慢交付。实际上往往恰恰相反。那些有意识地实践 演进式领域开发——把每个故事都视为建模机会的团队,往往能够显著更快地交付功能。
通过让领域模型始终与业务保持一致,团队可以降低意外复杂度,避免大规模重写,并保持一个能够安全、增量变化的系统。