在一次普通的 Code Review 里,我提出我从一篇讲解 Spec-Driven Development 的文章中受到启发,想把组件里复杂的业务逻辑抽离成一个 rules.ts 文件作为核心维护管理的源头文件。
没想到这个想法引来了团队里两位同学的不同回应------后端同学说"这更像是 Business-Driven Development",另一位前端同学说"其实这是面向 AI 编程的重构"。
我当时有点懵:我只是想让代码好维护一点,怎么突然冒出来三个不同的概念?它们说的是同一件事吗?还是各说各的?
这篇文章是我事后整理的一些思考,不是标准答案,更多是一次概念厘清的过程。
起因:一段让人"胃疼"的组件逻辑
场景大概是这样的:一个表单组件,里面有十几个字段,每个字段的显示、禁用、必填状态都依赖彼此的值,还受到用户角色、当前流程状态、后端返回的配置项等多个因素影响。
随着需求迭代,这些判断逻辑开始散落在 JSX 的各个 disabled、hidden、required 属性里,或者藏在 useEffect 的某个角落。改一个需求要跳好几个地方,测试也很难覆盖。
我的想法是:把这些判断统一收进一个文件。
javascript
// env: React + TypeScript
// scene: extract form business rules into a single file
// OrderForm.rules.ts
export type OrderState = {
items: CartItem[];
address: Address | null;
userRole: 'guest' | 'member' | 'vip';
flowStatus: 'draft' | 'pending' | 'submitted';
isPending: boolean;
};
// Can the order be submitted?
export const canSubmitOrder = (state: OrderState): boolean =>
state.items.length > 0 &&
state.address !== null &&
!state.isPending &&
state.flowStatus === 'draft';
// Why can't it be submitted? (for tooltip / disabled hint)
export const getSubmitDisabledReason = (state: OrderState): string | null => {
if (state.items.length === 0) return 'Cart is empty';
if (!state.address) return 'Please fill in the delivery address';
if (state.flowStatus !== 'draft') return 'Order has been submitted';
return null;
};
// Should the VIP discount field be shown?
export const shouldShowVipDiscount = (state: OrderState): boolean =>
state.userRole === 'vip' && state.items.length > 0;
这些函数有几个共同点:纯函数、无副作用、不依赖任何 UI 层的东西。它们可以被独立测试,也可以被 AI copilot 单独修改,不会误伤组件渲染逻辑。
然后 CR 的时候,争论来了。
三个概念,说的是同一件事吗?
Spec-Driven Development:先定规范,再写实现
后端同学最初提的是 Spec-Driven Development(规范驱动开发) ,但随即他自己也觉得这个词不太准。
SDD 的核心是:有一份"单一真相来源"(Single Source of Truth),开发围绕它展开。最典型的例子是 API 开发里先写 OpenAPI YAML,再生成 server stub 和 client SDK;或者先定 TypeScript 类型,再写实现。
我的 rules.ts 某种程度上也在做这件事------先定义"业务规则是什么",再让组件去引用它。这个文件就是那份 source of truth。
但 SDD 更多强调的是流程,而不是文件结构本身。它关心的是"你是不是先写了规范才动手写实现",而不是"你把规范放在哪里"。
Business-Driven Development:让代码说人话
后端同学后来改口说"其实更像是 BDD",这里他说的不是测试领域里的 Behavior-Driven Development(那个 BDD 特指用 Gherkin 语法写测试用例的实践),而是一种更广义的业务驱动 思想:代码要贴近业务语言,要让不写代码的人也能读懂意图。
这背后其实是 DDD(领域驱动设计)里"通用语言(Ubiquitous Language)"的落地------领域专家、产品、开发用同一套词汇描述同一件事。
放在我的场景里,这层意思是:rules.ts 里的函数名要用业务词汇。
javascript
// Business-oriented naming (recommended)
export const canSubmitOrder = ...
export const shouldShowVipDiscount = ...
export const isAddressRequired = ...
// Technical-oriented naming (harder to maintain)
export const checkFlag = ...
export const validateConditionA = ...
export const controlVisibility = ...
前者用业务动词命名,看名字就能理解意图;后者命名模糊,要深入读代码才能知道它在判断什么业务规则。
这个维度说的是语义,和放不放在一个文件里其实是两回事,但两者结合起来会更有价值。
AI-First Refactoring:为工具协作重新组织代码
另一位前端同学说的"面向 AI 编程的重构",是最近才开始被广泛讨论的工程实践,目前还没有一个统一的正式名字。
它的核心假设是:AI copilot 在处理小的、单一职责的、自描述的文件时效果最好 。一个 500 行的组件文件里混杂着 UI 结构、样式逻辑、副作用和业务判断,AI 改起来容易"误伤";而一个 60 行的 rules.ts,里面只有纯函数和类型定义,AI 一眼就能理解意图,改起来精准且可预测。
从这个角度看,rules.ts 是在为 AI 协作创造一个"安全操作区":
vbnet
OrderForm/
index.tsx ← UI structure, calls rules, doesn't contain logic
OrderForm.rules.ts ← pure business rules, safe area for AI to operate
OrderForm.test.ts ← only tests rules, no need to mount the component
OrderForm.types.ts ← shared type definitions
这个维度说的是工具链适配,和前两个是完全不同的维度。
那我的构想到底叫什么?
把三位同学的视角放在一起,我意识到他们说的其实是同一件事的三个面:
| 维度 | 概念 | 对 rules.ts 的意义 |
|---|---|---|
| 流程 | Spec-Driven | rules.ts 是规范,先写它再写组件 |
| 语义 | Business-Driven | rules.ts 里要用业务词汇命名 |
| 工具 | AI-First | 小文件单职责,让 AI 每次只改一处 |
但如果要给这个模式找一个最贴切的名字,我查阅资料后觉得它最接近的是业务规则外置(Business Rules Externalization) ,这是企业架构领域的成熟实践;在面向对象设计里,有时也叫 Policy Object 模式。
思路很简单:把"判断逻辑"从"执行逻辑"里分离出来,单独成文件,单独测试,单独演化。
这种模式以前在前端的存在感不强,因为 Redux、MobX 这类状态管理库的 action/reducer 结构在一定程度上替代了它。但在 AI copilot 普及之后,它的价值被重新放大了------不只是为了人类维护,也是为了让 AI 能精准地修改业务规则,而不是在一个巨型组件文件里大海捞针。
落地时的几个小取舍
这个模式并不是银弹,在考虑引入的时候有几个地方值得权衡,我目前的理解是这样的(不一定对):
适合放进 rules.ts 的:
- 返回
boolean的状态判断(canXxx、shouldXxx、isXxx) - 返回提示文案的逻辑(
getXxxMessage、getXxxReason) - 基于当前状态的派生值计算(
getXxxConfig)
不太适合放进去的:
- 需要调用 API 的异步逻辑(那更适合放在 service 或 hook 里)
- 直接操作 DOM 或依赖 React context 的逻辑(破坏了纯函数的特性)
- 过于简单的单行判断(
items.length > 0,直接内联更清晰)
还有一个细节:当 rules.ts 里的函数数量增多,可以考虑按业务子域继续拆分,比如 OrderForm.submit.rules.ts、OrderForm.display.rules.ts,而不是一个文件越堆越大。
延伸想了一些问题
在整理这些思路的过程中,我产生了几个新的疑问,暂时还没有答案:
- 规则文件里的测试应该怎么组织? 纯函数很好测,但当
OrderState的字段越来越多,构造 mock 数据会变得繁琐,有没有更好的测试策略? - 如果规则本身来自后端配置(比如 feature flag 或动态表单配置),这个模式还成立吗? 这时候"规则"本身是运行时数据,而不是编译时代码,边界就模糊了。
- 这和 Zod 之类的 schema 验证库是什么关系? 两者都在做"约束表达",但侧重点不同------Zod 偏数据合法性校验,
rules.ts偏业务状态判断。能不能配合使用? - AI 工具真的会更倾向于修改小文件吗? 这个假设我还没有系统性地验证过,只是直觉上觉得合理。
小结
回头看这次 CR 的对话,三位同学说的都有道理,只是各自在不同的维度切入。Spec-Driven、Business-Driven、AI-First,这三个标签并不是互斥的选择,它们描述的是同一个设计决策在不同语境下的意义。
把业务规则从组件里抽出来,这件事本身并不新鲜;但在 AI 工具成为日常开发协作者的今天,这种结构的价值被重新放大了。它既是给人类读者的清晰表达,也是给 AI 工具的精准操作界面。
这让我开始思考:我们在做代码组织决策的时候,"对 AI 是否友好",会不会慢慢变成和"可读性"、"可测试性"同等重要的考量维度?
参考资料
- Domain-Driven Design Reference - Eric Evans - Ubiquitous Language 与 Policy Object 的概念来源
- Specification pattern - Martin Fowler - 业务规则外置模式的早期描述
- Policy Object - Refactoring.Guru - 用对象封装条件逻辑的重构手法