mirror of
https://github.com/codestable/CodeStable.git
synced 2026-09-19 09:03:09 +08:00
248 lines
11 KiB
Markdown
248 lines
11 KiB
Markdown
# 编程通用范式:编排-计算分离与渐进结构化
|
||
|
||
> 本文档从具体语言实现中提炼出语言无关的编程范式。
|
||
> 这套范式适用于任何支持函数/模块/接口抽象的语言。
|
||
> 分卷:[扩展模型与参数设计](programming-paradigm-extensions.md) · [编写与重构规则](programming-paradigm-practices.md)
|
||
|
||
---
|
||
|
||
## 第一部分:核心模型
|
||
|
||
### 0. 基础约束:代码是写给人看的
|
||
|
||
所有规则的根基是一条认知事实:**人在单一时刻能处理的信息量是有限的**。代码的一切组织方式——函数拆分、文件划分、层次结构——最终都是为了让阅读者在任意时刻只需面对"刚好能理解"的信息量。
|
||
|
||
#### 单函数:一屏可读
|
||
|
||
一个函数应该在**一屏之内读完**,阅读者不需要滚动就能看到全貌。这不是审美偏好,而是认知约束——滚动意味着阅读者必须用记忆力去拼接上下文,记忆力是最不可靠的资源。
|
||
|
||
经验数字:
|
||
|
||
- **编排函数**:10-30 行最佳,每一行是一个动作名。超过 50 行几乎必然层级混乱。
|
||
- **计算函数**:不超过 50 行。超了说明它做的事情不止一件,应该继续拆。
|
||
- **硬上限**:单函数超过 100 行,无论什么理由,都是需要拆分的信号。
|
||
|
||
#### 单文件:一个文件一个角色
|
||
|
||
一个文件应该用**一句话说清它的职责**。说不清 → 文件该拆。
|
||
|
||
经验数字:
|
||
|
||
- **编排文件**(Workflow / Handler):50-150 行。它只做编排,超了说明业务逻辑混进来了。
|
||
- **计算文件**(领域服务 / 工具函数):100-300 行。一个内聚的计算主题。
|
||
- **超过 400 行**:几乎必然职责混合。不是"太长了",是"两件事混在一个文件里了"。
|
||
|
||
拆文件的判断方法:**数一下文件里有几组"不相关的函数"**。如果函数 A-D 互相调用但从不调用 E-G,而 E-G 互相调用也从不调用 A-D——它们就是两个文件。
|
||
|
||
#### 单次可见逻辑量
|
||
|
||
在任意一个抽象层级上,阅读者一次看到的**逻辑分支不应超过 7±2 个**(米勒定律)。
|
||
|
||
- 一个编排函数里,步骤超过 7 个 → 考虑分成"阶段",每个阶段是一个子编排
|
||
- 一个 if/switch 里,分支超过 5 个 → 考虑用注册发现或策略模式替代
|
||
- 一个模块的公开接口超过 7 个方法 → 考虑拆成更小的模块
|
||
- 一个函数同时操作超过 7 个概念(order、customer、payment...)→ 拆成两步
|
||
|
||
#### 信息密度的平衡
|
||
|
||
过疏和过密都有害:
|
||
|
||
- **过密**:一个函数里塞了加载、计算、持久化、异常处理、日志——阅读者被淹没
|
||
- **过疏**:每个操作都拆成单独的类和文件,3 行逻辑散落在 5 个文件里——阅读者被迫四处跳转拼接
|
||
|
||
**判断标准:阅读者理解一段逻辑需要同时打开几个文件?** 如果超过 3 个,说明拆得太碎了。所有规则(分层、拆文件、抽函数)都要在"单点信息过载"和"跨文件跳转"之间找平衡。
|
||
|
||
#### 文件内的信息布局
|
||
|
||
一个文件被打开后,阅读者在**前 10 秒**应该能回答:
|
||
|
||
1. **这个文件是干什么的**(文件名 + 顶部注释/类名)
|
||
2. **核心流程是什么**(编排函数在最前面,不要埋在 500 行之后)
|
||
3. **依赖了谁**(import / 构造参数一目了然)
|
||
|
||
具体做法:
|
||
|
||
- **编排函数/主函数放在文件最前面**,不要被辅助函数埋没
|
||
- **公开接口在前,私有实现在后**——阅读者先看"做什么",有兴趣再往下看"怎么做"
|
||
- **命名要承载意图**——好的命名让阅读者跳过实现。`compute_risk(order, customer)` 不需要看函数体就知道它干什么;`process(data)` 什么都没说
|
||
|
||
#### 核心判断标准
|
||
|
||
所有数字(50 行、400 行、7 个概念)都是启发式信号。真正的判断标准只有一个:
|
||
|
||
> **一个新加入团队的人,打开这个文件/函数,能否在 30 秒内理解它的意图?**
|
||
>
|
||
> 能 → 信息密度合适。
|
||
> 不能 → 要么太长(拆)、要么层级混乱(抽取)、要么命名不好(重命名)。
|
||
|
||
### 1. 三段式执行模型
|
||
|
||
任何有副作用的业务操作,都可以分解为三个阶段:
|
||
|
||
```
|
||
加载(Load)→ 计算(Compute)→ 持久化(Persist)
|
||
```
|
||
|
||
- **加载**:从外部世界(数据库、文件、网络)读取状态,装入内存结构
|
||
- **计算**:纯逻辑处理,不接触外部世界,输入决定输出
|
||
- **持久化**:把计算结果写回外部世界
|
||
|
||
这三个阶段的代码**不应混在同一个函数里**。混合是复杂性的第一来源——你无法单独测试计算逻辑,因为它和 IO 纠缠在一起。
|
||
|
||
**不同语言的实现方式不同,但分离原则不变**:
|
||
- 函数式语言天然做到了(纯函数 + IO Monad)
|
||
- 面向对象语言需要显式分层(Loader / Service / Persister)
|
||
- 脚本语言需要纪律约束(把 IO 调用集中到函数首尾)
|
||
|
||
### 2. 状态是显式的容器,不是隐式的环境
|
||
|
||
程序中流转的状态应该是一个**显式的数据结构**,在函数间作为参数传递,而不是藏在全局变量、线程本地存储、闭包捕获、或模块级 mutable state 中。
|
||
|
||
```
|
||
// 原则
|
||
f(state) → state'
|
||
|
||
// 而不是
|
||
f() // 偷偷读写某个外部的 state
|
||
```
|
||
|
||
这个显式的数据结构(我们称之为 **Context**)有三层内容:
|
||
|
||
| 层次 | 性质 | 示例 |
|
||
|------|------|------|
|
||
| 不可变输入 | 请求级标识,全程不变 | request_id, tenant_id, trace_id |
|
||
| 领域状态 | 核心业务数据,可变 | order, customer, account |
|
||
| 过程产物 | 节点间的中间结果 | risk_score, payment_result |
|
||
|
||
**Context 的边界判断**:一个值该不该进 Context,问一个问题——"这个值在不同请求之间会不一样吗?" 会 → 进 Context;不会 → 它是依赖,通过构造/初始化注入。
|
||
|
||
#### 框架提供的状态容器是合法的 Context
|
||
|
||
框架提供的响应式状态容器(Vue ref/reactive、React useState、Redux store)本质上就是 Context——它们是显式的、可追踪的、框架帮你管理的状态。**使用它们不违反本原则。**
|
||
|
||
不同技术栈的 Context 形态:
|
||
|
||
| 技术栈 | 状态容器(Context) | 编排层 | 计算层 |
|
||
|--------|---------------------|--------|--------|
|
||
| Vue | ref / reactive | methods / watch | computed / 纯函数 |
|
||
| React | useState / store | event handler / useEffect | 纯函数 / useMemo |
|
||
| 后端 Java | Context 对象 | Workflow 类 | 领域服务 |
|
||
| Rust | &mut Context | orchestrator 函数 | 纯函数 |
|
||
|
||
**违反本原则的不是"改 ref",而是绕过显式容器传递可变状态**:
|
||
|
||
- 模块级变量被多个组件/函数隐式共享
|
||
- 全局对象/单例被当作垃圾桶随处读写
|
||
- 隐式注入(provide/inject、ThreadLocal)传递可变状态而非只读值或修改函数
|
||
- 闭包捕获了外层的可变变量,且修改不可追踪
|
||
|
||
判断标准:**"这个状态的修改,是否可以从编排层的代码里一眼看出来?"** 可以 → 合法;不可以 → 隐式状态,需要招安。
|
||
|
||
### 3. 编排与计算分离
|
||
|
||
这是整套范式的核心原理。
|
||
|
||
**编排(Orchestration)**= 决定"做什么、以什么顺序、在什么条件下"。
|
||
**计算(Computation)**= 具体"怎么做"。
|
||
|
||
两者必须分处不同的代码单元(函数、模块、文件)。
|
||
|
||
```
|
||
// 编排层:只有流程骨架,每一行是一个动作名
|
||
function process_order(ctx):
|
||
validate(ctx)
|
||
check_risk(ctx)
|
||
charge(ctx)
|
||
ship(ctx)
|
||
|
||
// 计算层:具体逻辑,不知道自己在哪个流程里
|
||
function check_risk(order, customer) -> risk_score:
|
||
// 纯计算,无副作用,不依赖 context 容器
|
||
```
|
||
|
||
**关键约束:计算层不应接收 Context 容器。** 它只接收自己真正需要的参数。编排层负责从 Context 中取出参数、调用计算、把结果写回 Context。
|
||
|
||
精确表述:**计算层接收值、返回值,不接触状态容器。编排层负责从容器取值、调计算、把结果写回容器。**
|
||
|
||
```
|
||
// ✅ 计算函数:接收值,返回值,不碰容器
|
||
function compute_risk(order, customer) -> score:
|
||
score = 0
|
||
if customer.bindDuration < 30: score += 40
|
||
if order.amount > 10000: score += 30
|
||
return score
|
||
|
||
// ✅ 编排层:从容器取值 → 调计算 → 写回容器
|
||
function handle_submit():
|
||
score = compute_risk(order.value, customer.value) // 取值 + 调计算
|
||
risk_score.value = score // 写回容器
|
||
|
||
// ❌ 计算和编排混在一起
|
||
function compute_risk():
|
||
score = 0
|
||
if customer.value.bindDuration < 30: score += 40 // 直接读容器
|
||
risk_score.value = score // 直接写容器
|
||
api.charge(order.value.id) // 连持久化都混进来了
|
||
```
|
||
|
||
**只读派生不算"接触容器"。** 响应式框架中的派生计算(Vue computed、React useMemo)读取容器但不修改状态,数学上是 `f(state) → derived_value`,属于计算层而非编排层。
|
||
|
||
这条约束的意义:
|
||
- 计算函数的签名暴露了它的真实依赖,可读性极高
|
||
- 计算函数可以脱离 Context 独立测试
|
||
- Context 的变更不会波及计算层
|
||
|
||
#### 编排层是一个职责,不是一个固定的架构层
|
||
|
||
编排的本质是**"组装能力而不实现能力"**。这个职责可以由不同重量级的代码单元承担:
|
||
|
||
```
|
||
最轻 最重
|
||
│ │
|
||
调用点直接组合 提取组合模块 独立编排模块/类
|
||
(模板里写slot组合) (CopyableTip) (OrderWorkflow)
|
||
│ │
|
||
适合:1-2次使用 适合:3+次重复模式 适合:核心业务流程
|
||
不额外建文件 多一个小文件 独立文件,专职编排
|
||
```
|
||
|
||
三者做的事情完全一样——组装能力。区别只是**固化程度**。
|
||
|
||
后端倾向重量级编排(独立 Workflow 类),因为流程涉及事务边界、持久化时机、多入口调用,这些约束迫使编排逻辑必须集中管理。
|
||
|
||
前端组件组合通常没有这些约束——一个页面模板组合几个组件,不涉及事务、调用点通常就一个——所以调用点直接组合就够了。
|
||
|
||
**前端需要重量级编排的信号**:
|
||
- 多步表单/向导:步骤之间有依赖和条件跳转
|
||
- 复杂状态联动:改 A 要联动更新 B、C、D,不该散落在各个 watch 里
|
||
- 同一组合模式重复 3 次以上
|
||
|
||
选择哪个重量级,问三个问题:
|
||
1. **有没有事务/持久化/多入口等约束?** 有 → 独立编排模块
|
||
2. **这个组合模式重复出现 3 次以上了吗?** 是 → 提取组合模块
|
||
3. **都不是?** → 调用点直接组合,最简方案先行
|
||
|
||
### 4. 同层同级原则
|
||
|
||
基础约束(第 0 节)在代码层面的直接推论:**一个函数体内的每一行代码,应该处于同一个抽象层级**。
|
||
|
||
```
|
||
// ✅ 同一层级:都是"做什么"
|
||
validate(ctx)
|
||
check_risk(ctx)
|
||
charge(ctx)
|
||
ship(ctx)
|
||
|
||
// ❌ 层级混乱
|
||
validate(ctx)
|
||
score = 0
|
||
for rule in rules: // 突然掉到"怎么做"
|
||
score += rule.eval(order)
|
||
charge(ctx)
|
||
ship(ctx)
|
||
```
|
||
|
||
判断标准:**每一行能不能用一句自然语言概括为"做了什么"**——如果需要说"它遍历了一个列表然后累加了分数",那这段代码属于下一层。
|
||
|
||
---
|