策略模式和状态模式到底啥区别?拿审批流表单说个明白
审批流表单这东西,做过的都懂——不同节点,表单长得完全不一样。有的要填金额,有的要传附件,有的就一个"同意/驳回"按钮完事。
然后你就写了一坨 if-else,按节点类型渲染不同字段。能跑。
等节点类型从 5 种膨胀到 20 种,这坨东西谁看谁头疼,没人愿意碰。
想重构,翻了翻设计模式,策略模式、状态模式好像都能套上去。选哪个?
这俩长得太像了。像到不少人直接下结论"就是换了个名"。但选错了,代码会往一个很拧巴的方向长。
代码骨架先摆出来
策略模式——行为可替换:
// 根据"类型"选一个算法执行
const strategies = {
finance: (data) => renderFinanceFields(data),
hr: (data) => renderHRFields(data),
legal: (data) => renderLegalFields(data),
}
function renderForm(type, data) {
const strategy = strategies[type]
if (!strategy) throw new Error(`未知类型: ${type}`)
return strategy(data) // 选完就跑,一锤子买卖
}
状态模式——行为跟着状态走:
// 当前状态决定行为,行为跑完可能切到下一个状态
class ApprovalFlow {
constructor() {
this.state = new DraftState(this) // 初始状态
}
submit() {
this.state.submit() // 同一个方法,不同状态下干的事完全不同
}
setState(newState) {
this.state = newState // 状态自己决定"接下来去哪"
}
}
骨架确实像。都是把行为抽出去、干掉 if-else。
但。
谁在做决定?这才是根本差异
策略模式里,调用方选策略。你告诉它"用哪个",它就用哪个。选完了,策略之间没有半毛钱关系。Finance 不知道 HR 存在,HR 也不关心 Legal。
状态模式里,状态自己决定下一步。DraftState 知道 submit 之后要变成 PendingState,PendingState 知道审批通过了该切到 ApprovedState。状态之间有流转关系。
策略是平铺的,状态是串联的。
不是文字游戏。这个差异直接决定代码怎么长。
审批流表单拆开看
一个典型审批流表单,至少有两层逻辑搅在一块:
第一层:同一个审批节点里,按业务类型渲染不同字段。财务审批要金额、HR 审批要人员选择器。
第二层:审批流跑到不同阶段,表单的可编辑性、按钮组、校验规则全变了。草稿态能编辑所有字段,待审批态只读,驳回态只能改驳回意见。
第一层——纯策略。第二层——纯状态。
混着写?灾难。
策略模式搞字段渲染
不同业务类型的字段配置,天然就是平铺、互不关联的。策略模式刚好:
const fieldStrategies = {
finance: () => [
{ name: 'amount', type: 'number', label: '金额', required: true },
{ name: 'invoice', type: 'upload', label: '发票' },
{ name: 'reason', type: 'textarea', label: '申请事由' },
],
hr: () => [
{ name: 'employee', type: 'select', label: '涉及人员', required: true },
{ name: 'effectDate', type: 'date', label: '生效日期' },
],
legal: () => [
{ name: 'contract', type: 'upload', label: '合同文件', required: true },
{ name: 'counterparty', type: 'input', label: '对方主体' },
{ name: 'amount', type: 'number', label: '合同金额' },
],
}
function getFields(bizType) {
const strategy = fieldStrategies[bizType]
return strategy ? strategy() : []
}
新增业务类型?加一条配置,不碰已有代码。完事。
这就是策略模式最爽的场景——同一接口、多种实现、互相不认识。
状态模式管流程
审批流的阶段——草稿、待审批、已通过、已驳回——每个阶段表单行为完全不同。阶段之间有明确的流转路径。
这种东西硬用策略模式能不能写?能。但味道不对。
// ❌ 策略模式硬写审批阶段
const stageStrategies = {
draft: {
editable: true,
buttons: ['submit', 'saveDraft'],
onSubmit: (ctx) => { ctx.setStage('pending') }, // 策略里出现了流转逻辑
},
pending: {
editable: false,
buttons: ['approve', 'reject'],
onApprove: (ctx) => { ctx.setStage('approved') }, // 又是流转
},
}
// 每个策略都要知道"下一步去哪"
// 说明它们不是独立的——有耦合
// 流转逻辑散落在每个策略对象里,看着就别扭
状态模式怎么搞?
// ✅ 每个状态封装自己的行为 + 流转目标
class DraftState {
constructor(flow) { this.flow = flow }
getFormConfig() {
return { editable: true, buttons: ['submit', 'saveDraft'] }
}
submit(formData) {
if (!validate(formData)) return // 草稿态有自己的校验
this.flow.setState(new PendingState(this.flow)) // 下一步是待审批
}
}
class PendingState {
constructor(flow) { this.flow = flow }
getFormConfig() {
return { editable: false, buttons: ['approve', 'reject'] }
}
approve() {
this.flow.setState(new ApprovedState(this.flow))
}
reject(reason) {
this.flow.setState(new RejectedState(this.flow, reason))
}
}
// 调用方根本不用管现在是哪个阶段
flow.state.getFormConfig() // 当前阶段该渲染啥,状态自己清楚
区别出来了。
策略模式里调用方得做选择:"现在是 draft,那我用 draft 策略"。状态模式里调用方只说"给我表单配置",返回啥,当前状态说了算。
两层拼一块
实际干活,两层逻辑要组合。一个组件同时需要"按业务类型选字段"和"按审批阶段控制行为"。
function useApprovalForm(bizType, flow) {
const fields = getFields(bizType) // 策略层:拿字段
const { editable, buttons } = flow.state.getFormConfig() // 状态层:拿行为配置
const formFields = fields.map(field => ({
...field,
disabled: !editable, // 状态层控制能不能编辑
}))
return { formFields, buttons }
}
干净。两层各管各的。
加业务类型?改策略层。加审批阶段?改状态层。不会出现"改一个功能翻五个文件"的窒息操作。
怎么选
问自己一个问题:这些行为变体之间,有没有流转关系?
没有 → 策略。表单类型、排序算法、价格计算规则、校验策略——选一个用,用完拉倒。
有 → 状态。审批流、订单状态、编辑器模式切换、TCP 连接——当前行为取决于之前发生了什么。
还有个更快的判断法:你的对象会不会"变身"?
策略模式里,对象的行为被替换了一次就稳了。状态模式里,对象会不断变身——草稿变待审批,待审批变已通过。身份一直在变。
状态模式的代价
状态模式不是免费的。
每个状态一个类,状态之间要互相引用。5 个状态还行,20 个就开始头疼了。流转图一旦有环路、条件分支、并行状态——可维护性直线往下掉。
// 流转一旦复杂起来…
class PendingState {
approve() {
if (this.flow.amount > 10000) {
this.flow.setState(new SeniorApprovalState(this.flow)) // 大额多一层
} else {
this.flow.setState(new ApprovedState(this.flow))
}
}
}
// 条件分支散在各个状态类里
// 全局流转图只存在于你脑子里
// 状态一多,谁都看不懂完整流程
所以真实项目里,审批流超过七八个节点,一般不手写状态模式,而是上状态机引擎——XState、Robot 之类的。把流转关系集中声明,别散在各个类里:
// XState 风格:流转集中声明
const approvalMachine = createMachine({
initial: 'draft',
states: {
draft: {
on: { SUBMIT: 'pending' },
},
pending: {
on: {
APPROVE: [
{ target: 'seniorApproval', cond: 'isLargeAmount' },
{ target: 'approved' },
],
REJECT: 'rejected',
},
},
seniorApproval: {
on: { APPROVE: 'approved', REJECT: 'rejected' },
},
approved: { type: 'final' },
rejected: {
on: { RESUBMIT: 'draft' }, // 驳回可以打回去重提
},
},
})
// 流转图在一个地方,不用翻 N 个类
状态机说白了就是状态模式的工程化升级——把散落的流转逻辑收成一张声明式的图。
策略模式没这个烦恼。策略之间本来就没关系,加 100 个也就是多 100 条配置,不会出现"流转关系爆炸"。但策略模式扛不住"行为随时间演变"——它没记忆。
灰色地带
有些场景确实很模糊。
表单的校验规则就是。不同字段校验逻辑不同,这是策略。但校验规则可能跟着表单填写状态变——比如"选了加急,审批人字段变成必填"。
拆开处理就行:
// 策略决定"校验什么",状态决定"什么时候校验"
const validationStrategies = {
amount: (value) => value > 0 ? null : '金额必须大于0',
employee: (value) => value ? null : '请选择人员',
}
class DraftState {
getValidationRules(formData) {
const rules = { ...baseRules }
if (formData.urgent) {
rules.approver = (v) => v ? null : '加急必须指定审批人' // 动态调整策略集
}
return rules
}
}
别纠结"这到底算策略还是状态"。哪部分是静态映射、哪部分是动态流转,拆清楚就完了。
审批流做成可配置的
真实 B 端系统,审批流迟早要做可配置。业务方自己画流程图、定义节点类型、配置表单字段。
这时候策略和状态的组合会演变成更大的架构:
const flowConfig = {
nodes: [
{
id: 'apply',
type: 'form',
bizType: 'finance', // → 策略层拿字段
stage: 'draft', // → 状态层拿行为
transitions: [ // → 状态机流转配置
{ event: 'submit', target: 'review' },
],
},
{
id: 'review',
type: 'form',
bizType: 'finance',
stage: 'pending',
transitions: [
{ event: 'approve', target: 'done' },
{ event: 'reject', target: 'apply' },
],
},
],
}
function createFlowEngine(config) {
const machine = buildMachineFromConfig(config) // 配置 → 状态机
const fieldResolver = (nodeId) => {
const node = config.nodes.find(n => n.id === nodeId)
return getFields(node.bizType) // 配置 → 策略
}
return { machine, fieldResolver }
}
策略层变成可注册的插件——新业务类型注册新策略就行。状态层变成可配置的状态机——业务方画流程图等于在编辑状态机配置。
模式选对了才走得到这一步。选错了,到这儿早就重构不动了。
判断模型
纠结的时候画两条线:
X 轴:行为变体有多少。 2~3 种?if-else 就够了,别上模式。超过 5 种?必须抽象。
Y 轴:变体之间有没有时序依赖。 没有 → 策略。有流转 → 状态。都有 → 分层,各管各的。
购物车优惠计算是策略,订单生命周期管理是状态。游戏角色攻击方式是策略,存活状态是状态。道理一样。
把"平铺的变体"和"流转的阶段"分清楚,比记住模式名字重要得多。