功能开关:它是负债,不是配置
每个开关都会让代码路径翻倍,而开关本身永远不会被删。给每个开关定一个移除日期,比设计一套精巧的开关系统重要得多。
功能开关解决的问题很明确:让部署与发布解耦,能灰度、能回滚。它的问题同样明确:每加一个开关,可能的状态组合就翻一倍,而没人负责删掉它。
三种开关,寿命完全不同
| 类型 | 存活时间 | 例子 |
|---|---|---|
| 发布开关 | 几天到几周 | 新首页灰度 |
| 运维开关 | 几个月,可能永久 | 紧急降级某个依赖 |
| 实验开关 | 一次实验的周期 | A/B 测试 |
把三者混在一个系统里管理,是混乱的起点。发布开关必须被删除,它只是上线过程中的脚手架。
用日期而不是布尔值
给开关加一个到期字段,比任何文档约定都有效:
const FLAGS = {
newCheckout: { enabled: false, expiresAt: '2026-10-31' },
legacySearch: { enabled: true, expiresAt: '2026-12-01' },
} as const;
配合一个测试:过期的开关让构建失败。
for (const [name, f] of Object.entries(FLAGS)) {
if (Date.parse(f.expiresAt) < Date.now()) {
throw new Error('flag ' + name + ' is past its removal date');
}
}
这条断言把「以后清理」变成了「现在必须处理」。没有它,开关会永久留在代码里。
开关的读取要有默认值
function isEnabled(name: keyof typeof FLAGS): boolean {
const override = process.env['FLAG_' + name];
return override ? override === '1' : FLAGS[name].enabled;
}
默认值写在代码里,环境变量只做临时覆盖。反过来(默认关闭、靠配置打开)会导致新环境忘记配置就静默关掉功能。
代码形状:尽早分支,合并回收
开关只该出现在一个地方。散落的 if (flag) 会让删除变得几乎不可能。
// 好:一个地方决定实现
export const checkout = isEnabled('newCheckout') ? newCheckout : legacyCheckout;
// 差:实现内部到处都是分支
export function checkout(cart) {
if (isEnabled('newCheckout')) { /* ... */ }
if (isEnabled('newCheckout')) { /* ... */ }
}
前者的删除是删一行加删一个文件;后者要在几十处做撤并。
开关不该承载业务逻辑
如果某个开关已经开了三个月且没有任何关闭计划,它就不是开关了 —— 它是一项配置,应该合并进正式代码路径。继续叫它「开关」只会让人以为它可以随时删掉。
开关的价值在上线那几天。之后每多留一天,它就多一分让代码难以理解和测试的负债。

评论
…