领域模型与flow框架结合的一些构想

11 次阅读
领域模型设计

领域模型与 Flow 框架设计原则

适用读者:具有 5–10 年开发经验,已理解并使用自研 Flow 框架的中高级开发人员。

修订日期:2026-10-25。

本文回答一个设计问题:使用 Flow 开发业务时,业务能力放在哪里,应用层、领域模型、仓储和外部调用如何协作。

原则适用于接口、消息处理、定时处理,以及人工与系统共同推进的业务过程。文中引用实际的机型手册变更业务解释原则,案例中的模型划分及方法名均为示例,不要求其他业务照搬。

阅读顺序为:统一术语 → 明确架构分工 → 理解案例 → 判断行为位置 → 设计数据交互 → 组织跨多次操作的业务过程 → 评审。

一、统一术语

下表集中定义本文使用的概念。术语用于描述设计,具体类可以同时体现多个概念,一个概念也可能由多个类协作实现。

名词 统一定义 示例或设计用途
业务领域 我们研究和处理的业务范围,包含业务概念、活动和要求 机型手册与维护条目变更管理
领域模型 对某个业务范围的抽象,表达其中的概念、关系、状态和行为 手册、任务、条目组及其关系和业务能力共同构成模型
领域对象 领域模型中表达具体业务概念的对象 一份手册、一个变更任务、一个有效区间
实体 具有独立身份、需要持续识别的领域对象;属性变化后仍能识别为同一个对象 同一任务从准备中变为待生效,身份不变
值对象 通过属性值表达含义、不需要独立身份的领域对象,通常不可变 有效区间
领域服务 不适合由某个实体或值对象独立承担的业务能力,可以协调领域对象完成确定的业务判断、计算或操作 根据手册及其适用区间规划条目组变化
聚合 以一个实体为根,将需要共同维护一致性的实体和值对象组织成业务修改边界 条目组及内部条目可以作为候选聚合
聚合根 聚合中承担对外修改入口及内部约束维护责任的实体 候选条目组聚合中的 EntryGroup
业务规则 业务对行为和结果的要求,说明何时允许操作以及操作后应有什么结果 新版批准后安排旧版截止
业务不变量 需求中规定的、业务状态变化时必须守住的约束 同一机型最多一个未结束任务
限界上下文 明确一套业务名词、含义和规则适用范围的边界;范围内使用同一套模型理解和处理业务,跨范围时明确如何交换和解释信息 维护侧关注条目编写、审核和生效,运行评估侧关注匹配、适用条件和采用结果,可据语义及责任差异分别建模
应用层 组织一次业务操作的执行,负责输入准备、领域调用、保存及外部交互协调 具体 MessageService、Action、Config、业务 Stage 共同组织一次操作
仓储 提供领域对象的加载与保存能力,隔离具体存储细节 加载任务,保存变更方案及条目组
基础设施 数据库、HTTP、文件、消息等技术交互职责及其实现 由技术代码承载,不要求每种交互单独建立模块或封装对象

业务领域、领域模型与领域对象。 业务领域说明处理什么业务,领域模型说明如何表达这项业务,领域对象是模型中的组成部分。手册、任务和条目组可以共同属于一个模型,并不因此各自成为一个领域。一个业务领域也可以在不同限界上下文中采用不同模型。

实体与值对象。 二者是领域对象的不同建模类型。手册按身份识别,有效区间按起止时间及边界语义识别。“实体”不是“领域对象的实现”的同义词。值对象不要求独立 ID 或独立表;同一领域实体也不必对应一张表。

聚合与聚合根。 聚合划定共同维护一致性的对象范围,聚合根是其中一个实体承担的角色。例如 EntryGroup 可以既是实体,又是条目组聚合的根,无须再增加一个包装类。聚合可以只有根实体;有关系的对象也可以属于不同聚合,通过标识关联。只读查询可以直接返回展示所需的数据。

业务规则与业务不变量。 业务规则包括操作条件、计算方式和处理结果等要求。业务不变量是其中必须守住的状态约束,也是需求的一部分。例如任务可以创建、完成、作废,但同一机型始终最多一个未结束任务。“不变”指状态变化不能突破约束,不表示需求永远不能调整。约束也可以只针对特定状态,例如已生效条目组必须审核通过,而准备中的条目组可以尚未审核。

聚合根与领域服务。 聚合根具有身份,管理聚合状态和内部约束;领域服务通常没有自身业务身份,不保存某次处理的持续业务状态。修改条目组内部条目,应由组的业务入口检查状态;综合多个手册、任务方案和已有条目组计算新安排,可以由领域服务承担。领域服务可以调用聚合根的方法,但不能绕过根任意修改内部对象。两者的区别不在方法数量,也不在涉及几个实体。

领域服务与应用层。 领域服务协调对象以得出业务判断和结果;应用层协调加载、领域调用、保存及外部交互。二者都可能有顺序、分支和循环,要根据所作决定的含义区分。应用层使用 Flow 执行处理链,具体 MessageService 和 Action 已可以承担这些职责,无须额外增加转调层。

限界上下文与业务领域。 业务领域说明处理哪方面的业务,限界上下文说明一套模型和业务语言在哪个范围内成立。两者不要求一一对应,同一业务领域也可以根据不同的语义和责任划分多个上下文。不同业务对同一个现实对象可能有不同理解,可以各自采用合适的模型,不必把所有属性、状态和行为强行放进一个共享类。这里的“同一个现实对象”不等于“必须共用同一个领域实体”。

条目在不同上下文中的表达。 在手册维护中,条目关注内容依据、条目组、编辑、审核、生效和历史;在运行故障评估中,条目关注故障匹配、飞机适用条件、与其他条目的冲突及采用结果。两边可以引用同一条目标识,但不必共享包含全部行为的类。维护侧维护正式内容,评估侧依据取得的信息执行自己的评估行为,不能通过评估操作直接改写维护内容。具体交互要明确提供哪些事实、按哪个版本和时间理解数据、由谁维护。以上是边界分析示例;仅因展示字段或接口不同,并不足以划分上下文。

限界上下文与聚合。 前者界定模型和术语的适用范围,后者界定模型内部的一致性边界。一个上下文可以包含多个聚合,上下文不直接等同于实体、包或微服务。跨聚合的业务计算仍可以属于领域服务;跨上下文的交互通常由应用代码组织调用或消息协作,各上下文内部的业务判断由各自模型承担。

仓储与基础设施。 仓储提供加载与保存契约,实现负责存储映射;仓储接口引用领域类型是正常依赖。基础设施描述技术职责,不强制形成一个独立组件或模块。外部调用可以直接由 Action 实现,复杂度、复用或隔离需求出现后再抽取调用组件。

上述概念用于分析责任,不是必须逐一创建的类清单。模型边界与一致性术语参照 Eric Evans 的 DDD Reference。

二、架构定位:应用层使用 Flow,领域模型承担业务

原则:应用层负责组织执行,领域模型负责业务判断、计算和合法状态变化,Flow 提供执行机制。

判断依据: 按具体代码负责的工作区分:

  • 归应用层: 加载本次操作需要的数据,调用领域对象或领域服务,组织保存、外部调用及结果返回。这些工作由具体 MessageService、Action、Config、业务 Stage 承担。
  • 归领域模型: 判断业务上是否允许操作,执行业务计算,控制合法的状态变化。这些工作根据责任放在实体、值对象或领域服务中。
  • 归 Flow 框架: 按配置执行处理链,提供条件执行、中断控制和上下文传递等通用机制。具体业务的步骤安排由应用层配置,业务判断由领域模型完成。

职责分工概览

设计问题 职责划分原则
一次业务操作如何执行? 应用层使用 MessageService、Config、Action、业务 Stage 组织加载、领域调用、保存及外部交互
业务判断、计算和状态变化在哪里完成? 由领域模型承担,根据自然责任分配给实体、值对象或领域服务
聚合内部通过谁修改? 通过聚合根的业务入口维护内部约束,根可以委托内部对象执行
领域对象如何加载和保存? 应用层调用仓储;仓储还原对象和保存结果,隔离存储细节
外部接口如何调用? Action 组织调用时机及结果处理,按复杂度决定是否独立封装
处理链如何顺序执行、跳过或停止? 使用 Flow 的通用执行机制,不在通用框架中加入具体业务规则

表中按设计问题划分职责,不将实体、聚合或外部调用都视为同一种代码组件。术语说明概念,职责说明分工,类和处理链负责落实设计。

案例: “生成待维护条目组”可以由一条处理链组织:先加载任务、手册和旧组,再调用条目组规划能力,最后保存准备数据。时间轴如何切分、手册如何组合、复制哪组,属于规划能力;数据从哪里取得、结果如何保存,属于应用层和仓储。下一节给出理解这些例子所需的业务背景。

三、贯穿案例:机型手册与条目变更

本节仅为后续原则提供参照。业务领域是“机型手册与维护条目变更管理”;手册管理、条目维护和变更协作是其中的关注点,本例不据此划分多个独立领域或限界上下文。

业务背景与时间关系

每个机型维护自己的全部手册,分为 MEL、DDG、O 项。每份手册都有版本号、内容、批准状态及适用区间;区间由生效时间和截止时间确定,不限截止使用统一约定表示,例如 9999-12-31。本文在案例中用“适用区间”和“有效区间”表达同一种时间范围,代码名统一为 EffectivePeriod。

首次维护时,手册同时上传、生效时间一致;至少需要 MEL、DDG,O 项可选。后续各类手册独立维护,版本和区间可能不同。MEL 和 DDG 必须在拟生成条目的时间段内同时有效,O 项在该段有效时参与组合,不存在或已截止则不参与。

手册和条目组各有适用区间。 手册区间决定何时采用哪份依据;条目组区间按各手册的生效、截止边界投影到同一时间轴后切分。每段内手册组合固定,一份长期有效的手册可以作为多个相邻条目组的共同依据。

一次变更由任务组织,涉及一本或多本手册的上传、截止或有效期安排。新版批准后,按新版生效时间计算旧版截止安排。正常使用期间 MEL、DDG 通过新版接替保持有效,直接截止仅用于机型后续不再使用;截止 O 项会产生不含 O 项的新条目组。

新组复制其适用区间之前紧邻的条目组,由用户修改、审核;首次维护没有来源组,依据初始手册创建。准备工作必须在规定的提前时限内完成。生效前变更保存在 PENDING,ONLINE 保持原样;生效时统一落实手册变更和条目组变更,并完成任务。

同一机型最多一个未结束任务。生效前可作废整次变更,排除本任务准备数据,正式状态不变。生效完成后原任务不可作废,但可以通过新任务调整手册截止时间,并同步截断受影响的条目组;新截止时间不得早于当前时间,不能通过追溯截止改变过去的适用安排。手册截止调整及由此引起的条目组调整属于同一个变更任务,共同准备、审核并生效。截止调整仍须满足前述 MEL、DDG 持续覆盖等业务条件。历史条目组及内容保留且不可篡改,合法截止保留原内容和调整依据。

案例模型及能力

业务概念 建模方式 示例名称 业务能力与责任
机型 本例仅引用机型标识,不展开机型模型 aircraftTypeId 标识手册、任务和条目组所属机型,并限定任务互斥范围;不另设手册集 ID
手册 实体 Manual 表达一份确定版本的手册及其适用区间、批准状态;proposeExpiryChange(...) 检查新截止时间不得早于当前时间,返回待变更安排,纳入本次任务的完整变更方案
变更任务 实体 ChangeTask 保存本次待变更方案及进度;检查作废、生效条件,控制合法状态转换
条目组 实体;候选聚合根 EntryGroup 绑定适用区间、具体手册依据及复制来源;管理条目编辑、审核和历史保护
维护条目 实体;候选聚合内部对象 MaintenanceEntry 表达故障编号及维护内容,经条目组维护
有效区间 值对象 EffectivePeriod 日期包含、区间重叠及求交,不单独决定手册或条目组的正式可用性
任务发起 领域服务 ChangeTaskInitiator start(...) 根据同机型任务判断能否发起并创建任务
手册接替规划 领域服务 ManualSuccessionPlanner planReplacement(...) 检查新版批准状态,计算旧版的待生效截止安排
条目组规划 领域服务 EntryGroupPlanner plan(...) 切分区间、检查覆盖、选择手册组合及复制来源,形成变更方案

任务承载整次变更方案,表示记录变更内容、关联受影响对象并控制处理进度,不表示所有手册、条目组和条目都属于任务聚合。哪些对象需要随任务一起修改、哪些对象独立维护,仍应按各自业务约束确定。

类名及方法名用于明确责任,不代表现有代码 API,也不要求每项能力单独建立服务类。任务和条目组的最终聚合边界需要依据一致性、并发及维护方式确定。

案例中的手册类型和版本号是手册属性。新版上传后,保留旧手册及其内容,使历史条目组能够继续关联当时使用的手册依据。

版本接替示例

MEL M1、DDG D1、O 项 O1 从 1 月 1 日生效,暂不限截止。DDG D2 批准后计划从 7 月 1 日接替 D1。为清晰表达交接,示例采用左闭右开区间;实现时需统一边界语义。

手册 变更方案中的适用区间
MEL M1 [1 月 1 日,无限截止)
DDG D1 [1 月 1 日,7 月 1 日)
DDG D2 [7 月 1 日,无限截止)
O 项 O1 [1 月 1 日,无限截止)
条目组适用区间 手册组合 准备阶段的处理
[1 月 1 日,7 月 1 日) M1 + D1 + O1 规划旧组截止安排,保留内容及手册依据
[7 月 1 日,无限截止) M1 + D2 + O1 复制前组形成独立新组,供修改、审核

M1、O1 的区间跨越两个条目组,D1 与 D2 的交接形成切分边界。如果以后截止 O1,即使 MEL、DDG 不变,也要切出不含 O 项的新组。以上均先形成方案,任务生效前不修改正式安排。

四、业务行为应该放在哪里

原则一:业务要求落实到明确的领域行为

原则:先写清允许条件和预期结果,再明确由哪个领域对象或领域服务的什么业务方法负责。同一业务行为的判断和处理集中在领域模型中,不同入口调用该行为,避免重复编写业务逻辑。

判断依据: 同一业务行为可以由页面操作、外部接口请求、消息或定时处理触发。入口的数据准备、处理链和结果返回可以不同,但相同业务条件的检查及状态变化应调用同一处领域逻辑。例如不同入口作废任务,都调用 task.cancel(...),由该方法检查“已生效任务不能作废”。调整手册截止时间是截止变更行为,不是任务作废;手册与条目组的相关调整由本次变更任务统一承载。页面可以隐藏作废按钮,Action 也可以提前检查,但这些做法不能替代 cancel(...) 自身的状态检查;不要求不同入口共用整条 Flow。

案例: 下表中的方法名是职责示意,用于明确业务要求由谁承担。

业务要求 承担要求的领域对象或服务 明确的业务能力
同机型不能有两个未结束任务 ChangeTaskInitiator start(...) 根据该机型已有任务检查发起条件,只在允许时创建
已生效任务不能作废 ChangeTask cancel(...) 检查状态,拒绝已生效任务的作废操作
已生效安排允许后续截止调整,但新截止时间不得早于当前时间 ChangeTask、Manual、EntryGroupPlanner 本次变更任务统一承载手册截止调整及受影响的条目组调整;proposeExpiryChange(newExpiry, currentTime) 检查截止边界,plan(...) 校验手册覆盖并计算条目组变更,形成同一任务的完整待变更方案
MEL、DDG 同时有效,O 项可选 EntryGroupPlanner plan(...) 检查每个拟生成区间的手册组合和覆盖条件
新版批准后安排旧版截止 ManualSuccessionPlanner planReplacement(...) 计算以新版生效时间为边界的接替方案
新组复制前组且独立维护 EntryGroupPlanner、EntryGroup plan(...) 选择紧邻来源组,copyFor(...) 创建独立内容、身份及新依据
作废未生效变更,正式状态不变 ChangeTask cancel(...) 取消整套待变更方案;生效资格检查拒绝作废方案,作废不产生正式数据变更
历史不能修改、删除 EntryGroup;涉及历史手册内容时由 Manual 承担 编辑、删除等入口检查冻结状态;新的截止变更保留历史内容及调整依据

领域方法负责判断和产生合法的业务变化;应用层组织执行,仓储保存结果。领域方法完成校验或修改内存中的对象,不代表结果已经保存。

原则二:先找对象的自然责任,再考虑领域服务

原则:实体或值对象能够自然承担的行为,优先放在对象上;业务能力没有合适的单个对象主体时,使用领域服务。

判断依据: 涉及多个实体,不应作为使用领域服务的充分条件。聚合根可以协调多个内部实体,因此即使涉及多个实体,行为仍可能属于聚合根。应先判断该行为是否自然属于某个实体或值对象;只有找不到合适的对象承担这项业务能力时,再考虑使用领域服务。

Action、聚合根都可以组织协作,但应根据协作内容分工,不能任意选择:

协作内容 适合由谁组织
加载对象、调用业务能力、保存结果、处理外部交互 Action
为完成聚合自身的业务行为,协调内部实体和值对象 聚合根
不自然属于某个对象的跨对象业务判断或计算 领域服务

同一次操作中,Action 可以组织外层执行,聚合根组织内部业务行为,两者同时协作。例如 Action 加载条目组,调用 group.modifyEntry(...);聚合根检查组状态,再委托内部条目校验和修改;最后由 Action 调用仓储保存。判断依据是协作承担的责任,而不是涉及的实体数量。

案例: task.cancel(...) 维护任务自身状态;period.intersection(other) 完成区间计算;planner.plan(...) 综合多本手册和历史组得出新方案。无须再建一个只转调 task.cancel(...) 的领域服务。

原则三:聚合根守住内部修改边界

原则:外部通过聚合根的业务入口修改聚合,根负责内部约束,可以委托成员执行具体校验和计算。

判断依据: 划分聚合时,先看一次业务修改需要同时检查和保护哪些对象。需要由同一个业务入口统一控制的对象,可以考虑放在同一聚合中。仅有对象引用、数据库关联,或被同一条 Flow 使用,不足以作为划分依据。最终还要结合对象规模、独立维护需求及并发处理方式判断。

案例: 需求规定:“条目组冻结后,组内任何条目都不能再修改。”修改条目时,需要结合组状态判断;编辑与冻结并发发生时,也不能出现“组已冻结,先前发起的编辑仍成功提交”的结果。因此,可以考虑把条目组和内部条目放在一个聚合中,由 EntryGroup 作为聚合根。

Action:加载条目组
    ↓
group.modifyEntry(entryId, content)
    ├─ 检查条目组是否允许编辑
    ├─ 确认条目属于该组
    └─ 委托内部条目校验并修改内容
    ↓
Action:调用仓储保存修改结果

这里,聚合根统一控制组内修改,应用层组织保存。领域服务修改组内条目时,也应调用条目组的修改方法,由条目组检查是否允许编辑。不能绕过该方法直接修改条目内容。并发下如何可靠保存属于实现设计,本文不展开。

手册虽然是条目组的依据,任务也可能和条目组一起被 Flow 加载,但这些关系不表示手册、任务必须加入条目组聚合。是否属于同一聚合,要分别分析修改时需要共同保护的约束。若条目数量很大,或存在独立审核、提交需求,也应重新评估边界及一致性实现,不能仅凭本例直接确定最终设计。

原则四:领域编排得出业务结果,Flow 编排落实一次操作

原则:领域内部的完整计算保持内聚;应用层使用 Flow 组织其输入、调用与结果处理。

判断依据: 收集时间边界、切分区间、选择手册组合、确定来源组共同构成一项业务计算,可以在领域服务内部按步骤执行。加载、保存、调用外部接口则是用例执行协调。

案例: 生成准备组的一次处理链可以组织为:

Action:加载变更任务、相关手册及其适用区间、已有条目组
    ↓
Action:调用 EntryGroupPlanner.plan(...)
        ├─ 检查手册覆盖
        ├─ 切分时间区间
        ├─ 确定组合与来源
        └─ 按领域创建行为形成新组
    ↓
Action:保存任务方案及准备组,返回结果

这不是固定的三个 Action 模板。节点粒度根据可理解性、复用及独立处理需求决定。同一项业务计算的内部步骤,应由负责该计算的领域对象或领域服务组织,不要仅为了增加 Flow 节点而拆到多个 Action 中。

五、数据如何进入和离开领域模型

原则五:仓储还原和保存对象,应用层准备本次输入

原则:仓储承担对象还原与持久化;应用层加载本次用例需要的数据;领域行为决定新对象的业务身份、初始状态及继承方式。

判断依据: 读取已有对象,由仓储负责;组织本次操作需要的输入,由 Action 负责;根据业务要求计算结果、产生新对象,由领域模型负责。领域模型不依赖 Mapper、SQL、具体表结构或通用请求响应包装。

案例: 以下为调用关系示意:

ChangeTask task = taskRepository.findById(taskId);
List<Manual> manuals = manualRepository.findByAircraftType(task.aircraftType());
List<EntryGroup> groups = entryGroupRepository.findAffected(task);

EntryGroupChangePlan plan = planner.plan(task.proposal(), manuals, groups);
task.recordPlan(plan);
preparationRepository.save(task, plan);

manuals 是相关手册,每份手册包含自身的适用区间;task.proposal() 表示本任务提出的手册变更;EntryGroupChangePlan 是条目组规划结果。FlowContext 可以持有这些对象,但领域方法通过显式参数取得输入。示例中的 preparationRepository.save(task, plan) 仅简写本次准备结果的保存过程,不规定必须建立这个仓储,也不表示任务与所有条目组属于同一聚合。实际可由应用层组织调用相应仓储。

仓储接口引用领域类型,仓储实现也依赖这些类型以还原和保存对象,这是正常依赖。领域代码不引用仓储实现、Mapper 或数据库访问类;应用层通过仓储接口组织加载和保存。默认将领域仓储接口与模型放在领域代码范围,实现在仓储实现包或已有技术实现模块;展示专用查询接口可以放在应用代码范围。无需为遵守约定拆分 Maven 模块或统一新建 infrastructure 包。

例如 ChangeTaskRepository.findById(TaskId) 返回任务,save(ChangeTask) 保存任务。任务列表若只需机型、发起人和条目数量,可以由查询能力直接返回展示结果,不必还原完整聚合。

实体通常不自行加载和保存。领域服务确需查询业务事实时可以依赖仓储接口;应用层能明确准备的输入,优先直接传入。避免通过上下文或容器查找隐藏技术依赖。

原则六:外部调用由 Action 组织,按需要封装

原则:Action 组织调用时机、输入及执行结果处理,简单调用可以直接实现。是否抽取组件及接口,由复杂度、复用和隔离需求决定。

判断依据: HTTP 地址、序列化、认证及超时属于技术处理;根据数据判断版本接替是否合法、应该生成哪些条目组属于领域能力。两类代码应能区分,但不强制每次调用都增加一个对象和一组接口、实现类。

案例: Action 调用外部系统取得手册信息,处理响应和字段映射,再交给领域能力检查手册组合与时间安排。若多个 Action 重复使用,或认证、重试、协议变化开始影响业务处理,再抽取独立调用组件。

数据库、文件和消息等同样属于技术交互。基础设施描述这类职责,不要求形成固定的层次或类清单。

原则七:Context、Checker、Transfer 辅助执行,不代替领域行为

原则:领域方法不接收 FlowContext、不依赖上下文 key、不调用框架中断方法。执行上下文、校验组织和结构转换由应用代码及框架能力承接。

判断依据: 业务方法应能明确表达输入与结果,在不同入口复用。调用前执行了 Checker,不代表方法可以省略必要状态约束;Transfer 可以辅助复制字段,但新对象的身份、状态、手册依据和适用区间,仍须由领域方法按业务要求确定。

案例: 分别看作废任务与复制条目组两个场景:

作废任务时,Action 取得任务后调用 task.cancel(...)。任务方法检查自身状态:已生效则拒绝作废,未生效且允许作废才执行状态变化。如果使用 Checker 提前提示“不能作废”,Checker 应调用任务提供的判断方法,不再单独写一套判断。即使没有执行 Checker,cancel(...) 也必须完成必要检查。

复制条目组时,由条目组的创建方法确定新组 ID、复制来源、适用区间、手册依据和初始状态。Transfer 只辅助复制允许沿用的字段,不能直接把旧组全部字段复制后就当作新组使用。

Checker 辅助提前校验,Transfer 辅助字段复制;业务要求仍由领域方法保证。

请求字段是否缺失、格式是否正确等入口检查,可以由应用层的 Checker 完成,不必全部搬入领域模型。任务能否作废、条目组是否允许编辑等业务条件,必须由相应领域方法保证;Checker 若提前检查这些条件,应复用领域判断。

FlowContext 可持有领域对象、运行中间结果和技术信息。一次 Flow 执行中,跨 Stage 使用的数据放在根 FlowContext 中,供后续步骤读取。Context 中的数据只用于本次执行,业务状态的变化仍需调用仓储保存。领域结果由 Action 转成响应、异常或执行分支。

六、跨多次操作的业务过程如何组织

原则八:长期过程保存业务状态,每次触发执行一段 Flow

原则:跨越多次操作的业务过程,需要保存能识别该过程的业务身份、状态、待处理内容及处理记录。本例通过变更任务承载这些信息,其他业务按自身概念建模。每次人工或系统触发时,Flow 加载相关数据,调用领域行为并保存结果。数据库表如何组织,不要求与领域对象一一对应。

判断依据: 等待审核、等待生效可以跨请求和重启;Stage、AP 和停止标记只描述当前执行结构。自动触发还是人工确认,应由需求明确,不能从处理链连接关系推定。

案例: 上传手册、批准手册、修改条目、审核和到期生效可以分别触发处理。任务保存已完成事项及下一步允许操作。规定的提前时限在准备与审核流程中落实;审核通过只表示具备后续生效条件,正式安排仍在生效时统一更新。

例如,用户提交审核时,一次 Flow 加载任务、调用提交审核行为并保存任务状态,然后结束。审核人员批准时,另一次 Flow 加载该任务、调用批准行为并保存结果。到达生效时间后,系统再触发一次 Flow,加载任务并实施变更。贯穿整个过程的是已保存的任务及相关业务数据,每次执行根据这些数据继续处理。

领域模型与数据库表分别解决不同问题:领域模型表达业务概念、行为及约束;数据库表组织需要保存和查询的数据。例如,一个 ChangeTask 可以通过任务表、手册变更明细表和处理记录表保存,由仓储读取后还原为领域对象。表结构应结合查询、更新及一致性要求设计,无须照搬领域对象的字段和组合关系。

PENDING、ONLINE 是准备数据与正式数据的存储安排,不意味着必须建立两套领域实体。领域模型仍需表达准备、审核、生效等业务差异,仓储负责将领域对象与相应的存储数据进行转换。这里需要保存的是后续操作所依赖的业务数据,无须保存整个 FlowContext,也无须在等待人工处理期间保持 Flow 运行。

stopApAndSpMp() 只停止本次执行,不表示任务作废或审核拒绝。技术失败、业务拒绝和等待人工处理分别记录并处理,恢复时依据持久化状态而非保留上一次 Context。

七、评审标准

评审的重点是:业务要求由谁保证,一次操作如何执行,结果如何可靠保存。下表用于检查设计和实现,不要求每个项目具备全部术语对应的类,也不以类数量、接口数量或目录形式判断设计是否合理。涉及人工等待、外部交互或跨上下文协作的项目,再检查相应事项。

检查问题 评审时需要说明或展示的内容
业务概念及含义是否清楚? 说明主要对象的身份、属性、状态和关系。若不同业务对同一现实对象有不同理解,明确各自模型及信息交换方式,不因名称相同就强行共享一个实体类
每项业务要求由哪个业务方法保证? 写清允许条件、预期结果,以及负责的领域对象或领域服务和方法。页面、接口、消息、定时入口对同一业务行为调用相同领域逻辑,不要求共用整条 Flow
为什么由实体、值对象或领域服务承担? 能说明行为是否自然属于某个对象。聚合根可以协调内部对象;只有业务能力不适合由某个实体或值对象承担时,才考虑领域服务,不以涉及多个实体作为充分理由
聚合边界为什么这样划分? 结合具体修改说明:需要检查哪些对象、共同守住什么约束、通过谁修改。对象引用、数据库关联或同一任务使用,不足以证明应放进同一聚合
修改聚合内部对象时,是否经过聚合根检查? 展示实际调用路径。例如修改条目通过条目组检查状态和归属,再委托条目修改;Action、领域服务均不能绕过根直接改写内部数据
应用层和领域模型是否各自承担了合适的工作? MessageService、Action、Config、业务 Stage 组织加载、调用、保存及外部交互;实体、值对象和领域服务完成业务判断、计算及合法状态变化。同一项业务计算的内部步骤,应由负责该计算的领域对象或领域服务组织,不要仅为了增加 Flow 节点而拆到多个 Action 中
领域方法能否脱离 Flow 使用? 输入和结果明确,不接收 FlowContext,不读取上下文 key,不调用框架中断方法,不依赖 Mapper、SQL、表结构或通用请求响应包装
Checker、Transfer 是否只是辅助? 请求格式等入口检查可以放在 Checker;业务条件检查复用领域判断,即使未执行 Checker,领域方法仍能拒绝非法业务操作。Transfer 只复制允许沿用的字段,新对象的身份、来源、状态及适用范围由领域创建行为确定
加载已有对象与创建新对象是否区分清楚? 仓储根据存储数据还原已有对象;应用层准备本次操作的输入;领域模型决定新对象的业务身份、初始状态及继承方式。字段映射不能代替业务创建行为
领域对象与存储结构如何对应? 说明对象如何从一张或多张表还原、变更如何保存,不要求实体与表一一对应。准备表与正式表的划分应落实相应业务状态及生效要求,不因此机械建立两套领域实体
仓储、查询和外部调用是否按需要设计? 仓储隔离加载、保存及存储映射;展示查询可以直接返回所需结果。Action 组织外部调用,按复杂度、复用或隔离需求决定是否抽取组件,无须为每次调用建立接口和实现类
跨多次操作的业务过程如何继续? 明确保存哪些业务身份、状态、变更内容和处理记录,以及每次触发加载什么、调用什么、保存什么。下一次执行能依据持久化数据继续,不依赖保留上一次 FlowContext,也不在人工等待期间保持事务
审核、等待、生效、拒绝和作废是否被正确区分? 说明各状态的允许操作及转换条件,哪些由人触发、哪些由系统触发。框架停止不等于业务作废或事务回滚。案例中审核通过不提前改变 ONLINE,作废未生效任务排除整套准备变更,已生效任务不能作废
历史和时间边界是否得到保护? 说明区间边界语义、历史修改限制及时间检查的时点。案例中截止调整不得早于当前时间,手册与受影响条目组在同一任务内调整;复制新组不改旧组,截止变更保留原内容和调整依据

评审时应选择关键业务操作,沿“加载数据 → 调用领域行为 → 保存结果”的实际路径检查。例如作废任务,要能说明任务方法如何检查状态、取消哪些待变更内容,以及应用层如何组织保存结果。只说明“有 Checker”或“调用了 cancel”,不足以说明整项业务要求由谁保证。

验证应与上述责任对应:领域测试检查允许条件、非法操作、边界输入及业务结果,不需要构造 FlowContext;确需查询时可以替换仓储接口。应用层验证输入准备、领域调用、保存及失败处理。持久化和集成验证按需要覆盖对象还原、结果保存,以及跨请求继续处理。测试围绕实际业务风险选择,不按类数量机械补齐。