Files
codestable__codestable/asset/programming-paradigm-practices.md
2026-07-15 21:58:14 +08:00

12 KiB
Raw Permalink Blame History

编程通用范式:编写与重构规则

返回核心模型 · 扩展模型与参数设计

第四部分:编写规则

规则 1:值对象先行,实体随后

从需求中先识别值对象,再识别实体。值对象定义了系统的"度量衡",实体定义了系统的"角色"。

规则 2:最简 Workflow 先行

写一个最简编排把核心流程跑通,节点先用空实现。先达成结构共识,再填细节。

规则 3:逐个节点填充,每填一个跑一次测试

规则 4:同层同级,控制认知负荷

编排函数每一行保持同一抽象层级(见第 4 节)。所有长度和密度约束见基础约束(第 0 节),核心数字速查:

  • 单函数:编排 10-30 行(上限 50),计算不超过 50 行(上限 100)
  • 单文件:一个文件一个角色。编排文件 50-150 行,计算文件 100-300 行,超 400 行必拆
  • 逻辑量:单函数操作的独立概念不超过 7 个,分支不超过 5 个
  • 密度平衡:理解一段逻辑不应需要同时打开超过 3 个文件
  • 终极判断:新人打开这个文件,30 秒内能否理解意图?

规则 5:复制还是复用

判断:给它起个名字

把"相同的部分"假装抽出来,试着给它起个名字:

第一步:能起具体名字吗?
  ├─ 需要 Generic / Common / Base / Shared 前缀 → 不是真概念 → 复制
  └─ 有具体名字(CopyableTip、useAsync、Money)→ 进入第二步

第二步:不看调用点能理解它吗?
  ├─ 不能,要看谁在用才知道它干什么 → 意义依赖场景 → 复制
  └─ 能,它自己就说清了自己是什么 → 是独立概念 → 复用

两步都过了才复用。任何一步没过就复制。

这条原则在不同层级上的表现:

层级 复制(各自演化) 复用(同生共死)
编排/workflow 两个业务流程看起来像 → 复制,各走各的 —
固定结构组合 — CopyableTip = HoverTip + CopyButton,没有 if,改一个就该改全部 → 复用
领域计算 — "税怎么算"不因流程不同而不同 → 必须复用,单一真相源

判断错了怎么办:识别信号,内联回去

预测不可能总对。关键不是判断准,而是错了能快速纠正。错误的复用会发出四个明确的代码信号:

信号一:参数里出现了 boolean / enum 开关。 某个调用点说"我要不一样的行为",你加了 useIcon=true、feedbackMode="color"。开关越多,说明调用点分化越严重——当初的"同一个概念"假设已经不成立。

信号二:内部出现"只为某个调用点服务"的 if。 打开共享模块,发现某个分支只有一个地方在走。它不再是通用模式,而是特殊需求的拼盘。

信号三:改代码时脑子里在想"别影响别的调用点"。 这是最直觉的信号——你已经在为错误的复用买单了。

信号四:新人看不懂这个抽象。 需要看调用点才能理解它是干什么的,说明它不是独立概念。

出现任何一个信号,执行内联:

1. 把共享模块的代码复制回每个调用点(出现重复代码是正确的中间状态)
2. 删掉共享模块
3. 让每个调用点独立演化
4. 观察是否有新的真实共性浮现——演化后的共性才是真共性

默认策略:先复制,等待复用浮现

复制 → 发现是真共性 → 提取复用    容易,两份代码都是完整的
复用 → 发现不该复用 → 内联拆开    痛苦,N 个调用点已经依赖它

错误的复用比错误的复制代价大得多。 所以默认倾向复制——等到命名测试两步都通过、重复出现 3 次以上、而且没有出现上述四个信号时,再提取复用。

规则 6:新功能叠加,骨架不动

新功能以子流程、装饰器、Null Object 替换的方式叠加到现有 workflow 上。主 workflow 骨架尽量保持不变。功能上下 = 子流程的挂载或卸载。

在组件/模块层面同样适用:新功能是组合出新组件,不是往老组件里加 prop/开关。

判断方法:细化还是组合?

遇到"要改老模块还是建新模块"时,问一个问题:

新功能和老模块是"同一个概念的细化",还是"两个概念的组合"?

  • 细化 → 改老模块。比如 tooltip 支持不同方向(top/bottom/left/right),方向是 tooltip 本身的属性。
  • 组合 → 建编排层,把多个独立能力组装在一起。比如 tooltip + 复制、tooltip + 确认按钮。

实例:HoverTip + 点击复制

需求:已有 HoverTip 组件(鼠标悬停显示提示),现在某个页面需要 tip 内容可以点击复制。

分析:

  • HoverTip:鼠标悬停 → 显示文字 → 离开消失。核心是"被动信息展示"。
  • 点击复制:用户主动点击 → 写入剪贴板 → 反馈"已复制"。核心是"主动用户动作"。

一个是被动展示,一个是主动交互——不是同一个概念,是两个独立的能力。

❌ 在原组件里加 prop(瑞士军刀式膨胀)

  <HoverTip content="xxx" :copyable="true" />

  今天加 copyable,明天加 closable,后天加 hasAction,
  半年后 15 个 prop、8 个 slot、300 行模板。
  本质错误:把编排逻辑(哪个页面需要什么组合)塞进了计算层(组件本身)。

正确方向:HoverTip 提供 slot,CopyButton 作为独立能力,组合发生在调用点。

// CopyButton:纯"复制"能力,不知道自己会被用在哪里
component CopyButton(text):
    copied = false
    fn copy():
        clipboard.write(text)
        copied = true
        after 1.5s: copied = false
    render: button(onClick=copy, label=copied ? "已复制" : "复制")

最简方案——调用点直接用 slot 组合,不建新组件:

// 调用点就是编排层,决定 tip 里放什么
HoverTip:
    slot content:
        text("可复制内容")
        CopyButton(text="可复制内容")
    <span>悬停我</span>

HoverTip 和 CopyButton 各自不知道对方的存在,组合关系只在调用点体现。

判断链:何时提取组合组件?

1. 老模块有 slot / 扩展点吗?
   ├─ 没有 → 先加 slot(这是"细化",支持自定义内容是它本身的能力)
   └─ 有   → 在调用点直接用 slot 组合新能力

2. 这个组合模式重复出现了几次?
   ├─ 1-2 次 → 保持调用点组合,不建新模块
   └─ 3+ 次  → 提取组合组件固化模式,消除重复

只有当"tip + copy"的组合出现 3 次以上,才值得提取 CopyableTip:

// CopyableTip:固化重复的组合模式
component CopyableTip(content):
    render:
        HoverTip:
            slot content:
                text(content)
                CopyButton(text=content)
            slot default:
                <children/>

先用最简单的方式(slot 组合),等重复逼你封装的时候再封装。 这是规则 2(最简方案先行)在组件层面的体现。

通用原则:出现 1 次是用法,出现 3 次是模式,模式才值得封装。


第五部分:重构规则

规则 7:安全手法蚕食,不重写

操作顺序:重命名 → 抽取函数 → 移到新模块 → 清理新模块内部。

核心纪律:

  • 抽取函数和移到新模块是一个完整动作,只做抽取不做搬移 = 做了一半
  • 搬和改永远不在同一次提交——搬的 commit 只移动代码不改逻辑,改的 commit 只优化不移动
  • 每步之间运行测试,确保行为不变

安全手法按风险从低到高:

  1. 重命名:零风险,工具一键完成
  2. 抽取函数:极低风险,工具一键完成
  3. 引入参数对象:低风险
  4. 移到新模块:中低风险
  5. 内联后重新抽取:中风险,用于修正之前抽错的结构

规则 8:让结构浮现,再让结构裸露

从大函数中抽取小函数 → 结构浮现(编排逻辑从细节中露出来)。

把小函数搬到独立模块 → 结构裸露(编排模块只剩编排,打开就是流程图)。

最终目标:编排模块 20-30 行,一眼可读,每一行是一个动作名。所有实现细节在各自的独立模块里。

浮现了但没裸露,等于没做完。


第六部分:重构实操节奏

绞杀者模式

新结构从老代码外围长出来,逐步替换。整个过程老代码始终能跑、老功能始终不坏。

渐进阶段

阶段 动作 产出
0 画现状流程图(不动代码) 节点清单 + 每个节点的读写依赖
1 抽取函数 主函数变成 workflow 雏形
2 识别隐式状态(全局变量、线程本地、闭包捕获) 需要显式化的状态清单
3 引入 Context 结构体 所有状态传递显式化
4 分离加载/计算/持久化 纯计算函数可独立测试
5 搬到独立模块 老模块只剩编排
6 拆出 Loader / Workflow / Persister 完整分层结构

每个阶段都可停下来,代码始终可运行。

关键技巧

  • 特征测试做安全网:录制真实输入输出,每次改完对比行为一致性
  • 开关切流:新老流程并存,小比例流量先走新流程,逐步放量
  • 提交粒度越细越好:每搬一个函数就提交一次
  • 先别追求完美 Context:重构中期先用最简单的结构,稳定后再优化封装
  • 不值得重构的代码别动:又烂又稳定、无人修改的代码没有重构 ROI

附录:范式速查

原则 一句话
基础约束 函数一屏读完,文件一眼知主题,逻辑不用记就能跟上;理解一段逻辑不超过 3 个文件
三段式 加载、计算、持久化不混在一个函数里
显式容器 状态放在显式容器里,不藏在全局/闭包/魔法中;框架的响应式容器是合法 Context
编排-计算分离 编排说"做什么",计算说"怎么做",分处不同模块
编排是职责不是架构层 调用点组合→提取组合模块→独立编排类,按复杂度和重复度选择重量级
计算层不碰容器 计算层接收值、返回值;编排层负责从容器取值和写回;只读派生不算"碰"
Context at the Edge Context 只出现在编排层,计算层只接收真实依赖
同层同级 一个函数体内每一行同一抽象层级
注册发现 功能增减靠注册表,不靠 if/else
缺省行为 契约提供有意义的缺省,缺席用空实现顶替
契约稳定性 新方法给缺省、新字段走扩展区、大版本用适配器
复制还是复用 能起具体名字且脱离调用点能理解→复用;否则复制。默认先复制
错误复用的纠正 出现 boolean 开关 / 专属 if / 怕影响别人 / 新人看不懂 → 内联回去
细化改原模块,组合建新模块 同一概念的细化→改;两个概念的组合→slot 组合;重复 3 次→提取组合模块
领域计算必须复用 业务规则单一真相源,复制了就是定时炸弹
值对象先行 先识别度量衡,再识别角色
浮现再裸露 抽取函数 + 搬到新模块是一个完整动作
搬和改分离 移动代码和修改逻辑永远不在同一次提交