注册 登录
LUPA开源社区 返回首页

joejoe0332的个人空间 http://www.lupaworld.com/?22802 [收藏] [复制] [分享] [RSS]

我的博客

在Ruby中使用状态机

已有 1805 次阅读2013-4-3 10:11 |系统分类:IT技术|

有限状态机(FSM) 会用在我们周围的方方面面,如果你睁大眼睛,你会看到电子商 务处理订单,火车站的电子转换器(electronic turn styles )等,都会用到有限状态机。在我之前作为一个电子工程师的时候,有限状态机经常用于逻辑 们电路。使用有限状态机,能使那些意大利面条式的代码变得清晰,可读。

剖析有限状态机

要解释一个FSM其实很简单。正如名字所示,FSM包含了一个系统,模块,类,机器 等,以及数目已知的状态。从软件角度来说,当某个内部状态影响某个对象的行为时 ,它们就变得尤其重要了。从本质上讲,FSM有三个核心概念,即States(状态),Transitions(事 务)以及Events(事件)。

举一个例子,让我们来看看一个相当简单的苏打水机。在我
看来这其中的玄机就在 于如何正确地将一个对象确定为一个FSM。打眼一看,你可能想要选择这几个状态:机器关闭状态,
插入硬币,饮料出机,机器回到关闭状态。然而这不是一个好的状态机,尽管它描述了整个过程。因
为我们将一个苏打水机的行为和苏打水事务混进了一个对象里。

实现状态机

在Ruby的世界,处理已知问题的最好出发点当然是RubyGems。快速看一下Ruby Toolbox,我们发现有很多状态机gem。我个人偏好名为State Machine的gem.

我之所以选择这个gem而非其它,是结合了以往的经验和
它集成的功能。尽管没有直 接绑定Rails,它确实有ActiveRecord和ActiveModel集成。这些集成意味着我们可以直接使用一些不
错的功能,比如observers(Rails的观察者模式,译者注),validation(模型有效性验证),在
ActiveRecord中使用命名空间与数据库事务之间的映射。它与其它数据库访问层配合也很好,如
DataMapper,Sequel和Mongoid。

现在,我们要使用 state_machine 这个gem 对旧的ruby 对象增加有限状态机的特 性。

想象一下,我们的状态机会创建一个 sodaTransaction 类的实例,这个实例的初始 化状态是awaiting_selection. 我们的有限状态机会 在:awaiting_selection,dispense_soda,complete 状态 中转换。

require 'rubygems'
require 'state_machine'
class SodaTransaction
state_machine :state, initial: :awaiting_selection do
end
end

我们所知道的是当按下按钮(event :button_press)时,状态会同awaiting_section 转换到 dispense_soda状态,所以我们的代码是这样

require 'rubygems'
require 'state_machine'
class SodaTransaction
state_machine :state, initial: :awaiting_selection do
event :button_press do
transition :awaiting_selection => :dispense_soda
end
end
end



 

在这一点上,我们可以尝试我们的的soda交易在
irb。我发现了一个小窍门是运行
commandirb-I。从我们soda_transaction.rb文件所在的路径。这只是开始的irb会话的工作目录在当
前路径,可以方便地userequire“soda_transaction'with没有必要的其他路径。

require 'soda_transaction'
sm = SodaTransaction.new
puts sm.state
#=> awaiting_selection
sm.button_press
puts sm.state
#=> dispense_soda Huzza!我们实现我们想要的行为和淬火越来越近渴启动。有一件事,真的惹恼了我关 于soda贩卖机是我最喜欢的酒是脱销。本机等待我插入我的硬币,按下按钮,然后告诉我,我要解决 博士辣椒。我不想剥夺任何人的愤怒,以便让实施这种行为。
<
class SodaTransaction
attr_accessor :selection
state_machine :state, initial: :awaiting_selection do
event :button_press do
transition :awaiting_selection =>; :dispense_soda, if: :in_stock?
end
end
def in_stock?
stock_levels[@selection] > 0
end
def stock_levels
{
dr_pepper: 100,
sprite: 10,
root_beer: 0,
cola: 8
}
end
end

这里我们这里实现的就是,简单的在事务 :awaiting_selection 到 :dispense_soda之间放置一个检查。只需查询哈希表就可以知道我们需要的饮料是否仍有库存。

很明显,在实际操作种,我们需要减少库存数量,并把这个检查操作委托到应用的 其他地方。而在这里,简单的哈希查询已经足够。但是你会发现,例子里面的selection是一个实例 变量,它是在哪里被赋值呢?肯定是在事件里面吗?

我发现允许把这些额外参数传递给事件的最好办法是使用如下方法覆盖事件本身:

class SodaTransaction
state_machine :state, initial: :awaiting_selection do
event :button_press do
transition :awaiting_selection =&gt; :dispense_soda, if: :in_stock?
end
end
def button_press(selection)
@selection = selection
super
end
def in_stock?
stock_levels[@selection] &gt; 0
end
def stock_levels
{
dr_pepper: 100,
sprite: 10,
root_beer: 0,
cola: 8
}
end
end

由于方法在状态机被声明的方式,我们能够一旦完成任何自定义行为或新方法,简 单调用super来将调用传递回原始的事件。

享受Soda

经过稍微迂回来满足用户体验后,我们仍需要完成soda的整个事务。为了达到这个 目的,我们需要另外一个事件,当soda掉到盘子后自动触发。很简单,我们把这个事件设置为 dispense_soda状态到complete状态:

class SodaTransaction
state_machine :state, initial: :awaiting_selection do
event :button_press do
transition :awaiting_selection => :dispense_soda, if: :in_stock?
end
event :soda_dropped do
transition :dispense_soda =&gt; :complete
end
end
def button_press(selection)
@selection = selection
super
end
def in_stock?
stock_levels[@selection] > 0
end
def stock_levels
{
dr_pepper: 100,
sprite: 10,
root_beer: 0,
cola: 8
}
end
end 在irb中运行后,我们成功的实现了让soda掉至盘子,并且让客户好好享受。



 

require 'soda_state_machine'
sm = SodaTransaction.new
puts sm.state
#=> awaiting_selection
sm.button_press(:cola)
puts sm.state
#=> dispense_soda
sm.soda_dropped
puts sm.state
#=> complete 现在只有一件事情让我们苦恼的,就是管理我们的soda库存。从上面的例子,我们需要 减少cola的数量。
观察者和回调

幸运的是,使用state_machine gem处理这个问题是件区区小事。它提供了一系列类 似Rails过滤器的事务回调。

我们可以用下面的方法为:soda_droppedevent设置一个回调.

class SodaTransaction
state_machine :state, initial: :awaiting_selection do
after_transition :on =&gt; :soda_dropped, :do =&gt; :manage_stock
event :button_press do
transition :awaiting_selection =&gt; :dispense_soda, if: :in_stock?
end
event :soda_dropped do
transition :dispense_soda =&gt; :complete
end
end
def button_press(selection)
@selection = selection
super
end
def manage_stock
puts &quot;Removing 1 from the #{@selection} count&quot;
end
def in_stock?
stock_levels[@selection] &gt; 0
end
def stock_levels
{
dr_pepper: 100,
sprite: 10,
root_beer: 0,
cola: 8
}
end
end

我使用一个简单的Hash做股票管理,仅仅打印出SodaTransaction对象正在发送消息 来从当前股票中删除1。

虽然从视图的实际层面来看它感觉不是很好,但SodaTransaction现在就管理了soda 机器的库存。我的内心告诉我,我们可能想看到其它处理的方法。别让我在这简单的机器里出错,我 们已经给出了在我们看来是不错的实现了。

在我们需要更多东西的情况下,例如登记更多饮料售出的时间或者登记从购物口退 出,使用贯穿在模式的回调函数可能更合适。

class SodaStockObserver
def self.manage_stock(transaction, transition)
puts &quot;Removing 1 from the #{transaction.selection} count&quot;
end
def self.after_transition(transaction, transition)
puts &quot;#{transition.attribute} was: #{transition.from}, #

{transition.attribute} is: #{transition.to}&quot;
end
end
class SodaTransaction
attr_reader :selection
state_machine :state, initial: :awaiting_selection do
after_transition :on =&gt; :soda_dropped, :do =&gt;

SodaStockObserver.method(:manage_stock)
after_transition SodaStockObserver.method(:after_transition)
event :button_press do
transition :awaiting_selection =&gt; :dispense_soda, if: :in_stock?
end
event :soda_dropped do
transition :dispense_soda =&gt; :complete
end
end
def button_press(selection)
@selection = selection
super
end
def in_stock?
stock_levels[@selection] &gt; 0
end
def stock_levels
{
dr_pepper: 100,
sprite: 10,
root_beer: 0,
cola: 8
}
end
end

和你看到的一样,使用在transition回调函数
上,使用观察者更为强大,而且不必
花费大力气。要注意的一点是观察者代码是在after_transition方法里使用的transition.attribute
。这简单地代表了我们已经给到我们的状态机的东西,在这种情况下的“状态”。

正如前面提到,state_machine
gem可以与流行的数据库访问层如ActiveRecord进行
集成。部分集成方式是一个自动的回调过程。如果SodaTransaction对象继承于ActiveRecord,则定
义一个SodaTransactionObserver将自动为我们建立联系。

class SodaTransaction < ActiveRecord::Base
...
end
class SodaTransactionObserver < ActiveRecord::Observer
def after_soda_dropped(transaction, transition)
# Remove 1 from selection qty.
end
def after_transition(transaction, transition)
# Do whatever logging we need
end
end
 
实际用途

只要有状态机,就有很多事情可以做。涌现出来的项目有Spree, RestfulAuthentication 和一个非Ruby的EmberJS.

Spree在支付流程中使用了FSM,从获得各种支付信息到订单完成。你可以在FSM上用 钩子方法,插入自定义的状态以定制支付流程。

RestfulAuthentication (Devise之后是不是有人也在用?)实现了一个状态机 (acts_as_state_machine)。 它管理用户帐号的状态:‘pending’, ‘active’, ‘suspended’等等。

另一方面,尽管Ember不是Ruby实现,它在路由机制里使用了状态机。当用户在站内 浏览时,路由器使用URI来判断程序所处的状态。

曾有一段时间状态机整合进了ActiveRecord,但是在3.0之前被删除了。

在我的经验里,他们是开发者的好工具,我很好奇它们被如此少的使用。也就是说 ,FSM在错误的情况下使用只会让事情痛苦。建议是:开始阶段用一个状态数组,在示例上用一个状 态变量表示。不要担心实现完整FSM的开销,尤其前者会逐渐变得笨重。



英文原文:State Machines in Ruby

来自:开源中国社区

评论 (0 个评论)

facelist

您需要登录后才可以评论 登录 | 注册
验证问答 换一个 验证码 换一个

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部