FDE 的第一性问题:在写代码之前划定边界
这篇文章讲的不是「我用了什么技术」,而是「我在哪几个岔路口选了哪条路、
放弃了什么」。前者可以在任何一份 JD 里读到,后者才是 FDE 岗位真正的敲门砖。
一、约束
2020 年前后,我在做一个工业质量分析方向的系统。要做的事说起来很朴素:
让质量工程师在浏览器里看到统计过程控制(SPC)图,而不是每个人在自己电脑上
打开 Minitab 手动跑一遍。
朴素的需求,撞上一个不朴素的约束:
Minitab 只开放 C# 的 COM 组件接口,而 COM 组件无法被 Web 程序调用。
就这么一句话。它决定了后面所有的事情。
我先说结论:这个约束是资产,不是障碍。 如果 Minitab 当年开放的是一个
REST API,我很可能会写出一个糟糕得多的系统 —— 因为那样一来,把分析逻辑
散落到 Web 层就变得「顺手」了,而顺手往往是架构事故的开始。
约束把选择空间砍到只剩两条路,逼着我必须把理由想清楚。
二、决策一:桥接,还是重写?
摆在面前的是两个备选,而不是一个。
| 备选 A:用 C# 重写分析逻辑 | 备选 B:COM 桥接 | |
|---|---|---|
| 做法 | 自己实现 X̄-R 控制图、判异规则、过程能力指数 | 保留 Minitab 作为分析引擎,只解决「怎么调它」 |
| 代价 1 | 必须重新验证统计正确性 | 绑定 Windows + Minitab 授权 |
| 代价 2 | 客户已有的历史报告与新系统对不上 | 引入一个额外的进程边界要维护 |
| 代价 3 | 判异规则(Nelson rules)等细节的复现风险 | 单机 COM 组件,无法水平扩展 |
我选了 B。
选 B 的理由不是「重写太难」——那是个偷懒的理由,也不是个技术判断。
真正的理由是:在统计与质量领域,输出正确性是由参考实现定义的。
X̄ 控制图的控制限公式(X̄ ± 3σ/√n)谁都写得出来。但「一个点子连续 9 点落在
中心线同侧」算不算异常、「子组大小不齐时怎么处理」、「缺失值参与不参与移动极差
计算」——这些细节,Minitab 有它自己的答案。而这些答案,是质量部门过去几年
已经在用的答案。
所以备选 A 的 1 号代价不是我多花几个人月写代码,而是:我必须重新证明我的
实现是对的,而这个证明的成本远高于编码本身。 每一处细节差异都会表现为
「系统出的图和 Minitab 出的图对不上」,而现场没有人和你讨论公式,他们只会说
「这系统不准」。
这就是 FDE 与普通后端工程师在这里的分野。后者评估的是实现成本,前者必须
同时评估验证成本和信任成本。选 B 的真正含义是:不要去替换客户已经信任
的东西,而是让它进入客户的新的工作流。
我在别的场合把这个原则总结成一句话:
新系统要嵌入既有信任结构,而不是要求人们重新建立信任。
三、决策二:同步等待,还是异步取结果?
选定了「桥接」,下一个问题立刻浮出来:Web 请求打过来,怎么把结果拿回去?
这里我做了第二个选择:不返回结果,返回一个 OrderID。
流程是这样的:
- 前端调用 Web 接口,传一个唯一标识
OrderID—— 本质上就是向数据库写一条记录 - Windows 服务持续轮询这个 HTTP 接口取任务
- 服务拿到任务和参数后,调用 Minitab COM 组件做分析,生成图表与文字说明
- 前端循环查询这个
OrderID,拿到结果
用现在的话说,这是一张手写的任务表;用更刺耳的话说,这是我手搓的一个
没有持久化保障、没有重试、没有死信队列的消息队列。
但我今天仍然认为当时的判断是对的。理由是:
同步调用的代价是隐性的,而它的隐性恰好落在我控制不了的地方。
一个 HTTP 请求会阻塞在一个 COM 组件上。COM 组件的实例化几乎注定了分析是
串行的。那么:
- 并发用户数被 Minitab 的实例数硬锁死
- 请求超时时间变成了系统吞吐量的天花板
- 一个慢查询会连带拖垮所有请求
异步的代价则是显性的、可数的:多一次状态查询、要处理结果过期、前端要多写
一个轮询循环。
把代价从「不可控」挪到「可枚举」,这是我喜欢异步的唯一理由。
这里有个我后来才想明白的点:同步调用不是「没有队列」,它只是把队列藏了
起来 —— 藏在 Web 服务器的连接池里、藏在用户等待的进度条里、藏在 Nginx 的
超时配置里。异步做的不过是把这个队列摆到桌面上,让它变成一张你能看见、
能查询、能清理的表。
显式的排队比隐式的排队好。 因为隐式队列不会告诉你它满了,
它只会在某个下午突然集体超时。
四、决策三:把参数交给谁?
这是三个决策里最像「产品问题」的那个,也是我现在回看认为最有价值的一个。
接口里我暴露了三个可调参数:分析类型、子组大小、判异原则。
技术上看,这只是三个入参。但它实际上回答了 scoping 里最关键的问题 ——
哪些判断权属于系统,哪些判断权属于人?
| 参数 | 交给谁 | 为什么 |
|---|---|---|
| 分析类型 | 使用者 | 测什么、看什么图,是业务问题 |
| 子组大小 | 使用者 | 取决于抽样方案,由现场工艺决定 |
| 判异原则 | 使用者 | 取决于质量标准和客户要求 |
三个都交出去了。这意味着系统是工具,不是黑盒。
我当时没有意识到这个取舍的份量,只是觉得「参数当然要能传」。现在回看,
这条线画对了,理由是:
我没有任何依据替现场的质量工程师决定该用哪条判异规则。 那取决于他们的
客户是谁、执行的是什么标准、这批零件用在什么地方。系统一旦替他们选了,
系统就从「工具」变成了「需要被推翻的权威」。
这就是 FDE 意义上的 out of scope:
- In scope:把 Minitab 的分析能力搬到浏览器里,让现场不必在每台机器上装软件
- Out of scope:替使用者决定统计口径
划出去的部分,才是这个系统能被接受的原因。
我见过太多项目死在「我们做得比客户要求的更多」上。多做的那部分不是礼物,
是负债 —— 因为客户要为它负责,而它不在客户的控制范围内。
五、决策四:我漏掉的那一个
前面三个决策,今天回看我都还认。但有第四个,我当年根本没做。
原始的记录里,关于这件事只有一句话:
「写了一个 Windows 服务,开机后自动运行」
没有失败重试。没有日志。没有告警。没有 Minitab 进程崩溃或被占用之后的恢复。
没有「服务挂了谁来知道」的答案。
「开机自启」不是生产就绪,它只是「能被启动」。
这是一个长驻的、单点的、独占一个桌面软件授权的后台进程。它一旦挂掉:
- 前端会一直轮询,一直查不到结果,用户看到的是一个永远转的圈
- 没有人会收到通知
- 很可能要等到有人抱怨「系统坏了」才发现
这是我今天会完全重做的部分,也是这个案例里最有价值的失败记录。
我现在会在交付前就问完这几个问题:
- 它怎么失败? —— Minitab 没启动、被别的进程占用、COM 调用超时、内存泄漏
- 失败之后怎么被知道? —— 轮询任务超时算不算告警?心跳谁在收?
- 失败之后怎么恢复? —— 服务重启够不够?那批丢失的任务谁补偿?
- 交接给谁? —— 现场的人怎么判断「是系统坏了还是 Minitab 坏了」?
这四个问题没有一个和算法有关。但它们决定了这个系统是「一个能跑的 demo」
还是「一个能交给别人用的东西」。
FDE 交付的不是代码,是一个别人敢依赖的东西。
可靠性面不是上线之后补的,它是 scoping 的一部分。
六、我从这一个项目里提炼出的四条
写到这里,把这四条单独拎出来。它们和第几行代码无关,可以迁移到任何一次交付。
1. 先写「不做什么」。
in-scope 的清单谁都会写。真正定义系统的是 out-of-scope 那张纸。
每一条「不做」,都是一次主动的信任让渡。
2. 没有备选和代价的,不叫决策,叫偏好。
「我们用 COM 桥接」是偏好;「桥接 vs 重写,前者的代价是绑定 Windows
和单机授权,后者的代价是重新验证统计正确性,我选前者」才是决策。
面试官和客户都只对后者感兴趣,因为只有后者可以被质疑。
3. 评估成本时,把「验证成本」和「信任成本」算进去。
实现成本是最容易估的,也是唯一一个被默认估了的。
但一个能跑的实现,和一个被相信的实现,中间隔着的是验证成本。
4. 显式的丑陋胜过隐式的优雅。
一张长得像任务表的数据库表,比一个藏在连接池里的隐性队列好维护得多。
把系统里本来就存在的排队、重试、状态,明明白白地画出来。
七、关于本文的证据说明
本站的案例都标注证据等级。这一篇我也标:
L1 展示原始技术事实可核实,但量化结果缺失可以被核实的事实(来自我 2020–2021 年的两份原始技术笔记,现已归档进本仓库
的 git 历史):Minitab 仅开放 C# COM 接口、COM 无法被 Web 直接调用、Windows
服务开机自启并轮询 HTTP 接口、接口参数含分析类型/子组大小/判异原则、前端以OrderID 轮询取结果、服务调用 COM 组件生成图表与文字说明。
我不能提供的:这次交付的量化结果。
原始笔记里只有技术描述,没有「原来 ___ 分钟/次 → 现在 ___ 秒/次」这类对比
基线。我不会为了让这篇文章更好看而补一个数字上去 —— 编造一个听起来合理
的量化结果,比承认没有量化结果要严重得多。
同时我暂不标注客户与部署细节,因为那部分我无法在本文范围内给出可核实的
依据。这一案例的目标等级是 L2(可复现):脱敏数据 + 命令 + 版本 +
预期输出齐备,让别人能重跑。补齐后会更新这里的等级。
一个标成 L1 但每句话都能被追问的案例,比一个标成 L3 但经不起追问的案例
更有说服力。可信度比规模重要。
十一、后来发生的另一件事
写这篇文章时我意识到,本文讲的是接入层的取舍 ——
「一个分析能力怎么进系统」。
但还有一层更靠上的问题:所有分析能力怎么共享同一份数据?
那个决定比本文这个更大,代价也更贵 —— 因为我们最终选择的是
「不把数据搬进平台」,而这条路的默认答案恰好是最省事的那一个。
接入层解决的是「单个能力怎么进来」;架构层解决的是「数据形状由谁定义」。
后者是前者的前提 —— 如果数据形状不统一,接进来的能力越多,系统越乱。