+State——业务逻辑所在的地方
Infrastructure 文章 已经把一个原始的 "ACTIVE" 转换成了自己的 CustomerStatus.Active。
但也仅此而已。
我们仍然不知道这个客户是否允许下单,不知道当前有哪些操作可用,不知道哪个 State Transition 合法,也不知道某个业务流程真正依赖哪项 Selection。
这里开始进入 +State。
Infrastructure 把外部世界转换成一个受控的内部表示。+State 则把这些值放进自己的业务上下文中,建立关系并赋予含义。
Store 不只是数据容器
Section titled “Store 不只是数据容器”一个 +State 一开始往往看起来很普通:
type CustomerState = { customers: Customer[]; selectedCustomerId?: CustomerId; loading: boolean;};单独看,这个 State 只是保存了一些值。真正有意思的是,这些值能够回答什么问题:
当前选中了哪个客户?这个 Selection 还有效吗?哪些客户满足某项条件?当前允许哪些操作?一次 State Change 会带来什么结果?这些问题并不全是 Business Rule。Store 中的每个字段也不一定都是业务 State。比如 loading 仍然属于技术运行时状态——即使 Loading 不是 Boolean 已经说明,它通常也不是一个简单开关。
真正的区别在于,+State 不会停留在“保存值”。State Transition、派生和基于当前应用状态的业务决策也在这里产生。
State 通过关系、规则和派生获得业务意义。

Store 不是 Client-Side Cache
Section titled “Store 不是 Client-Side Cache”一种很简单的 State Management 理解是:
API ↓Store 保存 Response ↓Component 读取 Response这在某些场景完全够用。但从架构上看,这时的 Store 几乎只是 Backend 数据的 Cache:保存值,却没有为它们建立自己的业务含义。
我们对 +State 的理解更进一步:
Infrastructure ↓ +State ├── 拥有 State ├── 处理业务 Intent ├── 应用 Business Rule ├── 建模 State Transition └── 产生派生具体使用 Signals、NgRx、Redux、Zustand、RxJS 还是其他 State 技术,并不会改变这项职责。
Library 决定怎样管理 State。架构决定这个 State 承担什么职责。
Business Rule 放在这里
Section titled “Business Rule 放在这里”Infrastructure 已经完成了转换:
"ACTIVE" ↓CustomerStatus.Active现在,+State 可以把这个状态与其他信息一起解释:
CustomerStatus.Active+CreditHold.None+Account.enabled ↓ canOrder“这个客户是否允许下单”已经不是技术转换,而是系统自己的业务规则。
同样的还有:
- 一个订单允许取消吗?
- 一个数据记录允许删除吗?
- 某个 State Transition 合法吗?
- 当前业务状态下可以执行哪些 Action?
- 某个 Entity 是否满足特定业务条件?
一条 Business Rule 不会因为它最后只是控制 Button 是否 disabled,就突然变成表现层逻辑。
但这类规则最容易偷偷出现在这里:
disabled = customer.status !== CustomerStatus.Active || customer.creditHold !== CreditHold.None;只有一个 UI 时,这看起来没什么问题。
一旦同样的决定还要在 Ionic 表现层、移动端 UI 或其他地方使用,就会出现同一规则的多份实现。
更好的方式是由业务 State 直接提供明确结论:
canOrder;表现层随后只决定如何展示这个结果:禁用 Button、把 Action 灰掉,或者干脆不显示 Menu Item。
但“业务上是否允许”不应该再由表现层重新判断。
业务 State 与 Derived State
Section titled “业务 State 与 Derived State”并不是所有信息都需要额外保存。
从:
customers+selectedCustomerId ↓selectedCustomer可以直接派生出 selectedCustomer。
同样:
customers+filter+sort ↓visibleCustomersselectedCustomer 和 visibleCustomers 都不需要作为 Source State 的第二份拷贝保存,也不需要每次变化时手动同步。
它们可以由现有 State 计算出来。
这样可以减少重复事实以及随之而来的同步问题。一旦两个被保存的值表达同一份信息,就必须有机制永久保证它们一致。派生状态没有这个问题。

响应式 Filter 与 Sort 会更详细讨论 Query State 与 Derived State 的区别,以及为什么 Filter 和 Sort 自身也可能属于 State。
Selection 也可以属于 State
Section titled “Selection 也可以属于 State”一个 Selection 也不意味着必须把完整 Entity 再保存一次。
entities+selectedId ↓selectedEntity如果 Selection 对业务或应用级状态有意义,保存 ID 作为 Source State 就可能足够。选中的 Entity 再由它派生出来。
但 Selection 是否应该进入 +State,仍然取决于含义。表格里临时高亮的一行可能只是表现层状态。一个后续业务操作会依赖的 Selection,则拥有不同生命周期和职责。
Select by ID 会展示这样的 Select Flow 可以如何具体切分。
多个 Source 可以形成一个投影
Section titled “多个 Source 可以形成一个投影”派生并不局限于一个 Source。
articles+authors+categories ↓投影多个 State Source 可以共同形成一个可消费的投影。
关键不在于一共涉及几个 Store 或 Source,而在于最终产生的信息由谁负责。
来自多个数据源的 ViewModel 会展示多个响应式 Source 如何组合。Event-driven Projection 则讨论通过 Event 构建和更新的投影。
两种方式表达的是同一个核心思想:Consumer 不必知道底层 State 的原始结构。
不是所有东西都应该放进一个 Store
Section titled “不是所有东西都应该放进一个 Store”前面的论证不应该被理解为:一个 Feature 必须只有一个巨大的 +State。
Store Boundary 并不主要由文件大小决定,也不因为两个状态恰好使用相同 Entity 类型就自动应该合并。
更重要的问题包括:
- 哪些职责真正属于一起?
- 它们是否拥有相同生命周期?
- 是否因为同样原因发生变化?
- 谁拥有这些 State?
- 哪些 Consumer 会使用它们?
两个 State 可以都使用 Customer,却承担完全不同的职责。
反过来,多个数据类型也可能共同构成同一个业务状态。
当 Read Store 变得太大 会详细讨论这些 Boundary。
对 +State 来说,最重要的是:这个 Layer 描述的是一种职责类型,不是“必须只有一个 Store”的规则。
业务变化与 Intent
Section titled “业务变化与 Intent”+State 不只是拥有可读 State。业务上的修改意图也会在这里与当前 State 相遇。
deleteRequested ↓当前 State ↓允许 / 不允许 ↓State Change或者:
completeOrder ↓Business Rule ↓State TransitionIntent 首先只是一个意图。
“希望删除这个客户”与“这个客户已经被删除”不是一回事。
这项修改是否允许、最后应该形成什么业务状态,取决于当前 State 与规则。
CRUD Slices 系列 会具体展示 Create、Retrieve、Update 和 Delete Flow 如何沿着这些职责被切开。
对本篇来说,最重要的边界更简单:业务修改意图不应该等到 Component 或 HTTP Service 里才第一次获得含义。
ViewModel 在哪里出现
Section titled “ViewModel 在哪里出现”+State 可以提供 State 和派生,后续 ViewModel 会以此为输入。
但并不是每个 View 的完整投影都必须直接在 Store 中构建完成。
一个投影可以先把业务 State 转成可消费的信息。Application 层再为某个具体 View 组合多个这样的信息,或者暴露一个更适合该 View 的 API。
这条边界具体放在哪里,取决于实际切分。
ViewModel Aggregation 会更详细说明 ViewModel 如何由多个数据 Source 形成,而不把 Backend Structure 或内部 State Detail 泄漏到表现层。
对 +State 来说,真正重要的是:Business Rule 与业务相关派生,不应该等到某个具体 UI 被构建时才出现。
+State 可以做什么,不该做什么
Section titled “+State 可以做什么,不该做什么”+State 可以并且应该:
- 拥有业务相关的 Runtime State
- 应用 Business Rule
- 建模业务 State Transition
- 处理业务 Intent
- 生成 Derived State
- 提供相关 Selection
- 生成业务 Filter 与 Projection
- 响应业务相关 Event
- 集中那些不依赖具体表现层的规则
+State 不应该:
- 自己实现 HTTP Request
- Parse DTO
- 把外部 API Contract 直接穿过业务层
- 直接使用
HttpClient、fetch或 SDK 专用 API - 解释技术 API Error Code
- 直接控制 Router
- 打开 Dialog
- 显示 Snackbar 或 Toast
- 了解 DOM 或 UI Framework 细节
- 区分 Angular Material、Ionic 或 Bootstrap
- 承担本应属于 Application 的具体 View Orchestration
遇到边界模糊时,可以问一个简单问题:
如果明天 Material 换成 Ionic,这条规则还会成立吗?
如果会,它通常是 +State 的好候选。
这不是严格定义,但在实践中非常好用。
一套业务逻辑,多种表现层
Section titled “一套业务逻辑,多种表现层”当多个 UI 共同使用同一套业务能力时,这个切分的价值会特别明显。
在一个长期项目中,Angular Material 与 Ionic 曾经多年并行运行在同一业务基础上。
之所以能做到,是因为 Business Rule 没有分别在两个表现层里重复实现。
+State / \ / \ Material Ionic表现层决定如何展示。+State 提供业务结论。
如果 canOrder 在两个表现层里分别计算,就必须永久同步两份实现。迟早两个 UI 会对同一个业务问题给出不同答案。

同样的分离还会带来其他好处。
Business Rule 变得可发现。如果一个业务决定分散在 Component、Pipe、Form Validator、Service 和 Template 中,就无法在一个地方完整理解,只能靠重新考古来拼出来。
如果团队知道 canOrder 来自业务 State,也就知道应该去哪里找规则。
派生保持一致,因为不需要手动同步同一信息的多个副本。
而 Business Rule 可以脱离具体 UI 单独测试。Material 最终 Render 一个 Button,还是 Ionic Render 一个 Menu Item,对 canOrder 本身毫无影响。
这些好处并不是某个 State Library 带来的。
它们来自边界。
不是每个 State 都属于 +State
Section titled “不是每个 State 都属于 +State”给业务逻辑一个清晰归属,并不意味着所有变量都应该搬进 Store。
例如:
isDialogOpenhoveredRowactiveTabtooltipVisibleaccordionExpanded这些通常属于表现层 State。它们描述某个具体 UI 当前如何呈现。
当然,这条边界也不是绝对的。例如某个 activeTab 未来可能恰好代表一个业务相关 Selection。变量名本身并不能决定归属。
更有帮助的问题是:
这个 State 在脱离当前表现形式之后,仍然有意义吗?
如果答案是否定的,+State 大概率不是它自然的家。
与 Application 的边界
Section titled “与 Application 的边界”+State 拥有业务相关 State、Business Rule、派生、Selection、State Transition 与业务 Intent。
但具体 View 仍然不应该被迫了解整个 State。
一个 Customer List View 也许只需要:
customersloadingselectCustomer()createCustomer()Detail View 则需要同一业务领域的另一个切片。
这就形成下一条职责边界:+State 拥有并解释业务逻辑。Application 从中切出适合具体 Use Case 或具体 View 的 API。
这样既能让业务逻辑集中在一个清晰位置,也能避免每个表现层都理解其全部内部结构。