PAYATHON 2026

如何定义 custom state machine 并在 Order 实例上调用方法

支付小周

结论

问题不在状态转换本身,而在 guard 条件的执行上下文。定义 workflow 时使用的 anonymous class 只负责保存状态和转换规则。检查 payment_required? 时,receiver 必须是当前的 Order 实例。

建议按 guard 的类型分别处理:

  • guard 是方法名时,使用 order.public_send(:payment_required?) 调用。
  • guard 是代码块时,使用 order.instance_exec(&guard) 执行,或者把 order 显式传给代码块。

原因

在 Ruby 中,方法调用会作用于当前的 self。如果状态机内部直接执行:

send(:payment_required?)

Ruby 会在当前状态机对象上查找这个方法。如果代码运行在 anonymous class 的实例中,实际效果相当于:

state_machine.send(:payment_required?)

即使 Order 定义了 payment_required?,状态机对象也找不到它。

定义 DSL 时使用 class_eval 或 instance_eval,也不代表后续执行 guard 时会自动回到某个 Order 实例。状态机定义发生在类加载阶段,状态检查则发生在具体订单的运行阶段,两者的执行上下文不同。

推荐实现

让状态机显式接收当前业务对象,并始终在这个对象上执行 guard:

class StateMachine
  def initialize(owner)
    @owner = owner
  end

  def guard_passed?(guard)
    case guard
    when Symbol, String
      @owner.public_send(guard)
    when Proc
      if guard.arity.zero?
        @owner.instance_exec(&guard)
      else
        guard.call(@owner)
      end
    else
      !!guard
    end
  end
end

在 Order 中创建状态机实例时传入 self:

class Order
  attr_reader :state

  def initialize
    @state = :pending
  end

  def state_machine
    StateMachine.new(self)
  end

  def payment_required?
    true
  end
end

order = Order.new
order.state_machine.guard_passed?(:payment_required?)
# => true

这里的 @owner 是当前的 Order 实例,因此:

@owner.public_send(:payment_required?)

等价于:

order.payment_required?

在转换逻辑中使用

状态机的转换方法同样需要接收或保存目标对象:

class StateMachine
  def initialize(owner)
    @owner = owner
  end

  def transition_to(target_state, guard: nil)
    return false if guard && !guard_passed?(guard)

    @owner.instance_variable_set(:@state, target_state)
    true
  end

  private

  def guard_passed?(guard)
    case guard
    when Symbol, String
      @owner.public_send(guard)
    when Proc
      guard.arity.zero? ? @owner.instance_exec(&guard) : guard.call(@owner)
    else
      !!guard
    end
  end
end

调用方式如下:

order = Order.new

order.state_machine.transition_to(
  :payment,
  guard: :payment_required?
)

也可以传入代码块:

order.state_machine.transition_to(
  :payment,
  guard: -> { payment_required? }
)

零参数代码块由 instance_exec 执行,所以块内的 self 是当前的 Order 实例。

如果不想隐式切换 self,可以显式接收对象:

order.state_machine.transition_to(
  :payment,
  guard: ->(current_order) {
    current_order.payment_required?
  }
)

这种写法把执行上下文写得更清楚,通常也更方便测试和调试。

如果状态机定义存放在类上

workflow 规则可以存放在 Order 类上,但执行规则时仍需传入具体实例:

class WorkflowDefinition
  def initialize
    @transitions = {}
  end

  def transition(event, to:, guard: nil)
    @transitions[event] = {
      to: to,
      guard: guard
    }
  end

  def fire(owner, event)
    rule = @transitions.fetch(event)
    guard = rule[:guard]

    allowed =
      case guard
      when nil
        true
      when Symbol, String
        owner.public_send(guard)
      when Proc
        guard.arity.zero? ? owner.instance_exec(&guard) : guard.call(owner)
      else
        !!guard
      end

    return false unless allowed

    owner.instance_variable_set(:@state, rule[:to])
    true
  end
end

定义和调用方式如下:

class Order
  WORKFLOW = WorkflowDefinition.new.tap do |workflow|
    workflow.transition(
      :request_payment,
      to: :payment,
      guard: :payment_required?
    )
  end

  attr_reader :state

  def initialize
    @state = :pending
  end

  def request_payment
    WORKFLOW.fire(self, :request_payment)
  end

  def payment_required?
    true
  end
end

这样,anonymous class 或 workflow 对象只负责保存、查找和执行规则,不会被当成业务对象。

注意事项

如果 payment_required? 是 private 方法,public_send 会抛出 NoMethodError。可以把 guard 方法设为 public,也可以在确认允许调用非公开方法后改用:

@owner.send(guard)

不要把某个具体的 Order 实例永久保存在类级别的状态机定义中。类级对象可以共享不可变的 workflow 规则,但每次触发转换时都要传入当前实例:

workflow.fire(self, :request_payment)

状态机定义负责描述 workflow,Order 实例提供运行时状态和 guard 方法。只要执行转换时明确使用当前 Order 作为 receiver,payment_required? 就会在正确的对象上调用。

备注:内容仅供参考。