一次 CR 引发的思考:我的 rules.ts 构想,究竟属于哪种开发哲学?

小新 正七品 (知县) 2026-03-27 02:56 1 0 返回 码工码农
小新 正七品 (知县) 楼主
2026-03-27 02:56
第1楼

摘要: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; };

//Martin Fowler - 业务规则外置模式的早期描述 Policy Object - Refactoring.


在一次普通的 Code Review 里,我提出我从一篇讲解 Spec-Driven Development 的文章中受到启发,想把组件里复杂的业务逻辑抽离成一个 rules.ts 文件作为核心维护管理的源头文件。

没想到这个想法引来了团队里两位同学的不同回应——后端同学说"这更像是 Business-Driven Development",另一位前端同学说"其实这是面向 AI 编程的重构"。

我当时有点懵:我只是想让代码好维护一点,怎么突然冒出来三个不同的概念?它们说的是同一件事吗?还是各说各的?

这篇文章是我事后整理的一些思考,不是标准答案,更多是一次概念厘清的过程。


起因:一段让人"胃疼"的组件逻辑

场景大概是这样的:一个表单组件,里面有十几个字段,每个字段的显示、禁用、必填状态都依赖彼此的值,还受到用户角色、当前流程状态、后端返回的配置项等多个因素影响。

随着需求迭代,这些判断逻辑开始散落在 JSX 的各个 disabledhiddenrequired 属性里,或者藏在 useEffect 的某个角落。改一个需求要跳好几个地方,测试也很难覆盖。

我的想法是:把这些判断统一收进一个文件。

// 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 里的函数名要用业务词汇。

// 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 协作创造一个"安全操作区":

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-Drivenrules.ts 是规范,先写它再写组件
语义Business-Drivenrules.ts 里要用业务词汇命名
工具AI-First小文件单职责,让 AI 每次只改一处

但如果要给这个模式找一个最贴切的名字,我查阅资料后觉得它最接近的是业务规则外置(Business Rules Externalization) ,这是企业架构领域的成熟实践;在面向对象设计里,有时也叫 Policy Object 模式

思路很简单:把"判断逻辑"从"执行逻辑"里分离出来,单独成文件,单独测试,单独演化。

这种模式以前在前端的存在感不强,因为 Redux、MobX 这类状态管理库的 action/reducer 结构在一定程度上替代了它。但在 AI copilot 普及之后,它的价值被重新放大了——不只是为了人类维护,也是为了让 AI 能精准地修改业务规则,而不是在一个巨型组件文件里大海捞针。


落地时的几个小取舍

这个模式并不是银弹,在考虑引入的时候有几个地方值得权衡,我目前的理解是这样的(不一定对):

适合放进 rules.ts 的:

  • 返回 boolean 的状态判断(canXxxshouldXxxisXxx
  • 返回提示文案的逻辑(getXxxMessagegetXxxReason
  • 基于当前状态的派生值计算(getXxxConfig

不太适合放进去的:

  • 需要调用 API 的异步逻辑(那更适合放在 service 或 hook 里)
  • 直接操作 DOM 或依赖 React context 的逻辑(破坏了纯函数的特性)
  • 过于简单的单行判断(items.length > 0,直接内联更清晰)

还有一个细节:当 rules.ts 里的函数数量增多,可以考虑按业务子域继续拆分,比如 OrderForm.submit.rules.tsOrderForm.display.rules.ts,而不是一个文件越堆越大。


延伸想了一些问题

在整理这些思路的过程中,我产生了几个新的疑问,暂时还没有答案:

  1. 规则文件里的测试应该怎么组织? 纯函数很好测,但当 OrderState 的字段越来越多,构造 mock 数据会变得繁琐,有没有更好的测试策略?
  2. 如果规则本身来自后端配置(比如 feature flag 或动态表单配置),这个模式还成立吗? 这时候"规则"本身是运行时数据,而不是编译时代码,边界就模糊了。
  3. 这和 Zod 之类的 schema 验证库是什么关系? 两者都在做"约束表达",但侧重点不同——Zod 偏数据合法性校验,rules.ts 偏业务状态判断。能不能配合使用?
  4. AI 工具真的会更倾向于修改小文件吗? 这个假设我还没有系统性地验证过,只是直觉上觉得合理。

小结

回头看这次 CR 的对话,三位同学说的都有道理,只是各自在不同的维度切入。Spec-Driven、Business-Driven、AI-First,这三个标签并不是互斥的选择,它们描述的是同一个设计决策在不同语境下的意义。

把业务规则从组件里抽出来,这件事本身并不新鲜;但在 AI 工具成为日常开发协作者的今天,这种结构的价值被重新放大了。它既是给人类读者的清晰表达,也是给 AI 工具的精准操作界面。

这让我开始思考:我们在做代码组织决策的时候,"对 AI 是否友好",会不会慢慢变成和"可读性"、"可测试性"同等重要的考量维度?


参考资料

暂无回复,快来抢沙发吧!

  • 1 / 1 页
敬请注意:文中内容观点和各种评论不代表本网立场!若有违规侵权,请联系我们