策略模式和状态模式到底啥区别?拿审批流表单说个明白

27 阅读1分钟

策略模式和状态模式到底啥区别?拿审批流表单说个明白

审批流表单这东西,做过的都懂——不同节点,表单长得完全不一样。有的要填金额,有的要传附件,有的就一个"同意/驳回"按钮完事。

然后你就写了一坨 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 轴:变体之间有没有时序依赖。 没有 → 策略。有流转 → 状态。都有 → 分层,各管各的。

购物车优惠计算是策略,订单生命周期管理是状态。游戏角色攻击方式是策略,存活状态是状态。道理一样。

把"平铺的变体"和"流转的阶段"分清楚,比记住模式名字重要得多。