---
url: /zh/concepts/programming-model.md
description: Wow 领域模型里的五类函数、为什么决定与演化要分开，以及编译器如何把每个函数绑定到运行时。
---

# 决定与演化

Wow 的领域模型由普通的类和函数组成。本页说明有哪几类函数、每类能做什么不能做什么，以及 Wow 怎样调用它们。这里讲的是模型，不逐个介绍注解。

## 五类函数 {#five-kinds-of-function}

| 函数 | 输入 | 返回 | 能否做 I/O | 失败时 |
| --- | --- | --- | --- | --- |
| 决定 | 当前状态和一条命令 | 一个决定结果：事件、无变化或拒绝 | 能，通过构建模型时绑定的依赖 | 命令被拒绝或失败，什么都不写。 |
| 演化 | 状态和一个事件 | 无（它修改状态），或一个新状态 | **不能** | 这是模型的缺陷，立即失败。 |
| 投影 | 一次已提交的变更及其后的状态 | 无 | 能；它自己保证写入幂等 | 在这个投影里停放该聚合。 |
| 反应 | 一次已提交的变更及其后的状态 | 无 | 能；它自己保证效果幂等 | 在这个反应器里停放该聚合。 |
| Saga | 一次已提交的变更及其后的状态 | 要发送的命令 | 能 | 停放该聚合，除非 Saga 声明了它收到的这种拒绝在预期之内。 |

决定与演化属于聚合；投影、反应和 Saga 函数属于处理器，处理器订阅日志。它们怎样运行，见[订阅、失败与重放](./subscriptions)。

## 先决定，再演化 {#decide-then-evolve}

决定与演化是两个函数，因为它们回答的是不同的问题。

* **决定**回答“按现在的事实，会发生什么？”它读状态、检查规则，可以调用领域服务，产出事件。业务规则只放在这里。
* **演化**回答“既然发生了这件事，现在的事实是什么？”它把一个事件应用到状态上。事件永久保存，每次重建状态都要重放，所以演化看到同样的事件必须每次得出同样的结果。因此它不做 I/O，也没有依赖。

这种分离带来几条可以依赖的结论：重放日志能精确重建聚合当时的状态；冲突重试只在新状态上重新运行决定，别的什么都不重复；测试用事件和状态来写期望，与写模型时用的是同一套语言。

决定结果有三种，三种都是值，而不是异常：

* **事件**：按顺序追加的一个或多个事件。
* **无变化**：命令被接受，不追加任何东西。
* **拒绝**：带错误码、消息和可选字段 violations 的拒绝。错误码在模型上声明并列进 Manifest，所以客户端和其他服务事先就知道它。

聚合上没有错误处理函数。拒绝就是决定函数里返回拒绝的那个分支；失败之后的补偿动作属于 Saga。

## 决定能触及什么 {#what-decide-can-reach}

决定可以做 I/O，但只能以内核能掌控的方式接触外界：

* **领域服务**是命令函数的参数。编译器把它们变成生成的模型工厂的参数，宿主在构建模型时绑定一次，不会每条命令都去查找。
* **决定作用域**是这条命令自己的上下文。通过它，决定可以跨聚合预留唯一键、登记一个在追加没有成功时执行的补偿、为以后定时一条命令，以及给调用方返回额外的结果。
* **命令**可以只是命令体，也可以是准入后的命令；后者还带着准入确定的内容：它寻址的聚合、调用方的作用域和请求事实。

身份从不来自命令体。命令寻址的聚合在准入时由路由或消息决定，所有者和空间来自已认证的调用方。原因见[身份、租户与访问](./identity)。

### after 钩子 {#after-hooks}

after 钩子在命令函数之后、在同一个状态上运行，可以往同一个决定结果里追加事件。它是决定的一部分：它的事件和命令函数的事件一起演化、一起追加；它失败时整条命令失败，什么都不写。一组命令都要追加的事件，适合用它。

### 生命周期命令 {#lifecycle-commands}

每个聚合不用写代码就能处理几条生命周期命令：删除、恢复、转移所有者、转移空间和打标签。它们的决定与演化由内核提供，改的是聚合的元数据，元数据随快照一起保存。聚合默认处理全部这些命令，除非它只列出其中一部分。设置和取消定时，也同样是每个聚合都有的系统命令。

## 可变或不可变的状态 {#mutable-or-immutable-state}

演化函数有两种写法：

* **可变**（默认）：演化函数拿到状态，修改它的字段；嵌套的集合原地更新。
* **不可变**：演化函数拿到状态和事件，返回一个新状态。

两种写法都安全，因为不可变通常提供的保证，在这里由内核而不是状态类提供：

| 保证 | 状态可变时内核怎样做到 |
| --- | --- |
| 失败不会留下改了一半的状态 | 每次尝试都从缓存里取出状态，只有这次尝试提交后才放回。失败的尝试丢弃这个实例，下一条命令重新加载状态。 |
| 不会有两条命令同时修改一个状态 | 每个聚合有一条车道：它的命令按到达顺序逐条执行。 |
| 每个版本都有稳定的视图 | 版本提交时，内核把状态序列化一次。快照和订阅拿到的是这份视图，而不是活的实例。 |

可变状态的代价是：没有什么能阻止决定函数直接改字段。这样的改动不经过事件，内存里的状态会与日志渐渐不一致，直到下次重新加载。只在演化函数里改状态；命令函数或 after 钩子修改了状态的测试，测试 DSL 会让它失败。

## 两种写法 {#two-ways-to-write-it}

同一个模型可以在类上用注解写，也可以用 Kotlin 的 Decider DSL 写。两者编译成同一个聚合模型：一张命令函数表、一张演化函数表、身份规则、创建语义和守卫。用注解时，编译器在构建期生成绑定代码；运行时不扫描，也不经反射调用。

Java 代码使用同样的注解，编译器既读 Kotlin 源码也读 Java 源码。

## 函数怎样被调用 {#how-a-function-is-called}

很多函数只是纯计算：只凭状态就能决定的命令、把字段复制到内存的投影。它们不应该为协程付出代价。所以编译器按签名为每个函数选定三种调用形态之一，运行时不再逐次判断：

| 形态 | 识别依据 | 调用方式 |
| --- | --- | --- |
| 普通 | 普通函数 | 在聚合的车道上内联调用，不挂起，也不切换线程 |
| 挂起 | `suspend` | 在当前协程里调用；只有真正挂起时才让出线程 |
| Reactor | 返回 `Mono`、`Flux` 或 `Publisher` | 经 Reactor 适配模块调用 |

普通函数被假定为不阻塞。做阻塞 I/O 的函数，例如用 JDBC 写入的投影，要标上 `@Blocking`，运行时改在虚拟线程上执行它。阻塞而没有标记的函数，会拖住同一线程上的所有其他聚合。

内核本身只在真正的 I/O 处挂起。状态已在缓存里、决定函数又是普通函数时，一条命令从车道到追加是一段直线代码，追加是它唯一要等待的地方。

## 事件随时间变化 {#events-change-over-time}

日志永久保存，而事件类会变。所以读取旧事件是模型的正常部分：

* 事件的身份是它的**名字** `context.aggregate.EventName`，而不是它的类。重命名或移动类不影响历史；旧名字作为别名仍然可读。
* 事件声明一个**修订号**。结构变化时，你写一个从上一个修订号到下一个修订号的升级函数，编译器检查从修订号 1 到当前修订号的链条没有缺口。
* 已经没有类的事件类型，在它的聚合上声明为**退役**。读取时跳过它：重放忽略它，订阅越过它。

这些是你的模型的变化，不是 Wow 格式的变化。Wow 自己的存储格式或线上格式只在大版本之间变化，由迁移层处理。
