如何定义 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? 就会在正确的对象上调用。
备注:内容仅供参考。
