`
foo
  • 浏览: 25257 次
  • 来自: 北京
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

跨越边界: 对 Rails 进行扩展 分析 acts_as 插件

阅读更多

简介: Java™ 编程语言一直以来都是一个很出色的“熔炉”,它具有用于集成的丰富和强大的功能 —— 从用于集成企业库的依赖性注入容器,到 Enterprise JavaBeans (EJB) 技术,再到 Eclipse 的组件模型。通过使用大量这样的理念和架构,Java 开发人员率先采用新的方法将完全不同的软件库和组件组合成一个整体。但是 Java 开发人员并没有对优秀的集成技术造成垄断。本文通过审视一个名为 acts_as_state_machine 的流行插件来了解 Ruby on Rails 插件的工作原理。

 

 

在我撰写这篇文章的时候,德克萨斯州和俄克拉荷马州正从一场持久的暴风雪的影响中慢慢缓解过来。司机们又开始出现在马路上,他们不仅仅担心路面上的冰,还害怕其他性急的驾车者。三天的 “冬眠” 之后,我的生活开始慢慢恢复正常。但当从使用 Java 语言转换到使用 Ruby 后,不久,我便体验到了另一种“寒意”。当我使用 Java 项目时,总是可以找到能够解决一些小范围问题的特殊的 Spring 库或 Eclipse 组件。当 Ruby on Rails 刚出现的时候,我经常需要亲自编写它。令人高兴的是,这种“寒意”也开始慢慢消失,这要归功于一种有效的插件架构,很多人使用它来对 Rails 进行扩展。

关于本系列

在 跨越边界系列 文章中,作者 Bruce Tate 提出这样一种观点,即当今的 Java 程序员们可以通过学习其他方法和语言很好地武装自己。自从 Java 技术明显成为所有开发项目最好的选择以来,编程前景已经发生了改变。其他框架影响着 Java 框架的构建方式,从其他语言学到的概念也可以影响 Java 编程。您编写的 Python(或 Ruby、Smalltalk 等语言)代码可以改变编写 Java 代码的方式。

本系列介绍与 Java 开发完全不同的编程概念和技术,但是这些概念和技术也可以直接应用于 Java 开发。在某些情况下,需要集成这些技术来利用它们。在其他情况下,可以直接应用概念。具体的工具并不重要,重要的是其他语言和框架可以影响 Java 社区中的开发人员、框架,甚至是基本方式。

如果您曾经花了些时间研究过 Rails,那么一定会注意到 ActiveRecord 中的 acts_as 命令。尽管 ActiveRecord 与处理持久性有关,您总是希望将行为添加到类中,而不仅仅进行数据库存储和检索。比方说,通过使用 acts_as_tree,可以将类似树的行为添加到具有 parent_id 属性的类中。通过使用 ActiveRecord 模型中的 acts_as_tree,可以动态地添加方法来管理树,比如检索父记录或子记录的方法。在过去一个月里,我可以找到能够解决投票、版本化、Ajax、复合键以及不受基本 Rails 支持的所有特性的插件。

Rails 中的扩展模型是使用 Ruby 语言构建在特性之上的,与使用 Java 语言构建的模型完全不同。在本文中,我将剖析 acts_as 插件,您可以了解扩展模型的内部。我没有构建端对端场景,而是提供了部分产品示例以涵盖更多方面,并使您认识真实的插件以及如何在真正的生产代码中使用它们。

状态机 API

正如很多人所了解的那样,状态机 是一种关于系统状态的数学表达式。状态机混合了表示状态的节点以及节点间的转换。在任何一个给定的时间,状态机具有一个活动状态,也称为当前状态事件 触发状态之间的转换。为解释这个概念,我将展示我当前工作的一个组成部分:开发和维护 ChangingThePresent.org (CTP),这是一个非盈利性组织和捐赠者的平台(参见 参考资料)。CTP 让非盈利性组织可以提交关于其组织的信息和一些捐赠品(例如癌症研究员的一小时义诊时间或者捐赠给一名学生的书本),这就 使捐赠者仅使用一辆购物车就可将其慈善捐赠作为礼物转到他人的名下。收集所有这些信息会导致逻辑问题,所以我选择使用状态机来简化工作流程。

要解决这个问题,我使用了第三方插件,由 Scott Barron 编写,名为 acts_as_state_machine(参见 参考资料)。像很多 Rails 插件一样,acts_as_state_machine 结合使用了 Ruby 的功能和 Rails 专有的特性,不仅提供了一个库,还提供了特定于域的语言 (DSL),从而为用户提供了良好的体验。

一个用户向 CTP 提交内容(submitted 状态)。然后 CTP 管理员接收该内容并进行编辑(processing 状态)。如果 CTP 进行内容编辑,非盈利组织应该允许这些更改(nonprofit_reviewing 状态)。当 CTP 或非盈利性组织接受这些内容时,CTP 就会将这些内容显示在站点上(accepted 状态)。图 1 显示了状态机的图形表示:


图 1. CTP 状态机
CTP 状态机 

使用这个插件,我可以直接对我的类对象进行装饰,使用 DSL 表示不同的状态,在状态之间进行转换,而且事件会触发这些转换。清单 1 显示了我用来在 CTP 中管理非盈利组织的状态机的简化版本:


清单 1. 示例状态机

				
class Nonprofit < ActiveRecord::Base

  acts_as_state_machine :initial => :created, :column => 'status'
  
  # These are all of the states for the existing system. 
  state :submitted            
  state :processing           
  state :nonprofit_reviewing  
  state :accepted             
  
  event :accept do
    transitions :from => :processing, :to => :accepted
    transitions :from => :nonprofit_reviewing, :to => :accepted
  end
  
  event :receive do
    transitions :from => :submitted, :to => :processing
  end

  # either a CTP  or nonprofit user edits the entry, requiring a review
  event :send_for_review do   
    transitions :from => :processing, :to => :nonprofit_reviewing
    transitions :from => :nonprofit_reviewing, :to => :processing
    transitions :from => :accepted, :to => :nonprofit_reviewing
  end  


您以前可能没有见过所有这些 Ruby 特性,但是这种语言可以非常好地描述状态机工作流程。您可以看到每一个状态的描述,然后是状态机支持的事件,这之后,是每个事件将要触发的一系列转换。

每一条语句表示有效的 Ruby 语法。类定义之后,将看到 acts_as_state_machine :initial => :created, :column => 'status'。作为一名 Java 开发人员,您可能觉得查找方法调用而不是方法定义有些奇怪。Ruby 在类层次上将这些方法调用引用为。Ruby 通常在加载类时使用宏来为类添加功能。事实上,方法定义 —— def —— 仅仅是 Ruby 的宏。

接下来,将会看到一系列状态,例如 state :submitted。这些都是方法调用,每一个都将符号 作为一个单独的参数。(符号是用户定义的名称。)event 命令也是一个方法调用,使用 符号(定义事件名)和闭包(定义转换)作为参数。

每个转换都是在散列表之后的一个方法调用。在 Ruby 中,使用 key => value 对表示散列映射,用逗号分隔,并用括号 {} 括起来。当将散列映射用作函数调用的最后一个参数时,括号是可选的。可以看到这个方法 —— 状态、转换和事件 —— 与闭包和散列映射结合起来,可以形成一个良好的 DSL。

要使用状态机,可以实例化一个 Nonprofit 对象并在其上为每个事件调用方法,后跟一个 !,如清单 2 所示:


清单 2. 操作状态机

				
>> np = Nonprofit.find(2)
=> ...
>> np.current_state
=> :submitted
>> np.receive!                              
=> true
>> np.accept!
=> true
>> np.current_state
=> :accepted


! 是方法在一个步骤中修改和保存属性的 Rails 约定。所以对状态机插件的要求非常明显。我需要:

  • 能够方便放置状态机代码的位置。
  • 指定我的类方法的方式(DSL 需要使用类方法)。
  • 将实例方法附加到 Nonprofit 或任何其他目标类的方法。

本文接下来的内容将向您介绍插件。如果您希望下载代码并继续学习,请下载 acts_as_state_machine 插件。(参见 参考资料 中到 Scott Barron 站点的链接,并按照他的指导通过 Subversion 获得插件。)导航到 trunk/lib,将看到 acts_as_state_machine.rb 文件。在 trunk/init.rb 中找到初始化代码。我们只需要这两个文件。

Acts_as plug-ins

原则上讲,所有的 acts_as 插件工作原理相同。始终执行下面的步骤构建一个 acts_as 模块:

  1. 创建一个模块。以 acts_as_ 作为名字的开头。
  2. 在某些初始化代码中,打开 ActiveRecord 基类并添加 acts_as_ 模块。
  3. 在 acts_as_ 函数(比如 acts_as_state_machine)中扩展目标类的行为。

快速浏览一下 init.rb 中的初始化代码,如清单 3 所示:


清单 3. acts_as_state_machine 的初始化代码

				
require 'acts_as_state_machine'

ActiveRecord::Base.class_eval do
  include ScottBarron::Acts::StateMachine
end


代码将打开核心 ActiveRecord 类(ActiveRecord::Base)并添加 acts_as_state_machineclass_eval 方法打开类并在 class. Whew 上下文中运行类中的闭包。这看起来有些过于复杂,实际应用中,这个概念很简单:代码打开 ActiveRecord 基类并在ScottBarron::Acts::StateMachine 模块中混合。在 Ruby 中,可以快速打开并重新定义任何类。

由于增加了灵活性,这种功能是 Ruby 最好的优点之一。但是这个功能同时也是一种缺点。太多的灵活性将导致代码难以理解和维护,所以要谨慎使用。现在,打开 acts_as_state_machine.rb 文件来查看都混合了什么代码。

初始化模块

现在,我将避开实现状态机的具体细节,将主要介绍状态机与插件的接口。清单 4 显示了模块定义和状态机本身的接口:


清单 4. 模块结构

				
module Acts                        #:nodoc:
  module StateMachine              #:nodoc:
    class InvalidState < Exception #:nodoc:
    end
    class NoInitialState < Exception #:nodoc:
    end
    
    def self.included(base)        #:nodoc:
      base.extend ActMacro
    end
    
    module SupportingClasses
      class State
        attr_reader :name
      
        def initialize
          ...
        end
        
        def entering
          ...
        end
        
        ...
      end
      
      class StateTransition
        attr_reader :from, :to, :opts
        
        def initialize
          ...
        end
        
        def perform
          ...
        end
        ...
      end
      class Event
      ...
        def fire
          ...
        end
        
        def transitions
          ...
        end
        ...
      end


在 清单 4 的顶部,可以看到一个嵌套的模块定义,但是没有基继承层次结构。相反,可以将模块附加到任何现有的 Ruby 类。如果对这个概念还比较陌生的话,可以将模块看作是一个接口,外加该接口的实现。关于模块的一个好处就是可以将其功能附加到任何已有的 Ruby 类,并且可以根据您的需要添加,没有数量限制。还可以使用类的已有功能。这种技术叫做 mixing in。C++ 使用多种继承来提供与之类似的功能,但是非常复杂。Java 的创建者通过消除多重继承解决了这种复杂性。通过使用模块,可以享受到多重继承的优点而无需面对令人头痛的复杂性。诸如 Smalltalk 和 Python 这样的语言也支持 mix-in 继承。

清单 4 其余的部分展示了深入实现状态机的一般细节。您只需要知道这些类提供了状态机的独立实现。其余的代码更加有趣,因为它将状态机的接口公开给插件的客户机。

Acts_as 模块

回顾一下插件制作者需要的三个条件:放置实现的位置,公开 DSL(类方法)的方法以及为状态机公开实例方法的方法。这些包括清单 3 中起作用的事件方法。清单 4 提供了放置实现的位置。下一个代码片段将处理 DSL。

acts_as 插件架构具有一个定位点:acts_as 宏。 acts_as 插件的客户机将通过方法调用将这个方法引入到目标类中。在本文的示例中,我调用了 清单 1 中 Nonprofit 类的 acts_as,使用了下面的代码:

acts_as_state_machine :initial => :created, :column => 'status'


现在看一下清单 5,它为 acts_as_state_machine 提供了 ActMacro。该类处理模块属性并引入不同的类和实例方法。


清单 5. 添加 acts_as

				
module ActMacro
  # Configuration options are
  #
  # * +column+ - specifies the column name to use for keeping the state (default: state)
  # * +initial+ - specifies an initial state for newly created objects (required)
  def acts_as_state_machine(opts)
    self.extend(ClassMethods)
    raise NoInitialState unless opts[:initial]
    
    write_inheritable_attribute :states, {}
    write_inheritable_attribute :initial_state, opts[:initial]
    write_inheritable_attribute :transition_table, {}
    write_inheritable_attribute :event_table, {}
    write_inheritable_attribute :state_column, opts[:column] || 'state'
    
    class_inheritable_reader    :initial_state
    class_inheritable_reader    :state_column
    class_inheritable_reader    :transition_table
    class_inheritable_reader    :event_table
    
    self.send(:include, ScottBarron::Acts::StateMachine::InstanceMethods)

    before_create               :set_initial_state
    after_create                :run_initial_state_actions
  end
end


清单 5 中的模块具有一个方法:acts_as_state_machine。该方法执行下面五个任务:

  • 引入类方法
  • 处理状态机异常
  • 管理属性
  • 引入实例方法
  • 处理 before 和 after 过滤器

acts_as_state_machine 方法首先引入类方法。(可以在清单 6 中看到详细的方法清单。)接下来,该方法处理异常。在本例中,客户机没有指定初始状态时会发生异常(惟一的情况)。简单跳过继承属性(我将稍后深入介绍)。self.send 方法引入实例方法。(清单 7 显示具体细节。)before 和 after 过滤器是 ActiveRecord 宏,可以在 ActiveRecord 创建记录之前和之后调用set_initial_state 和 run_initial_state_actions

回到 write_inheritable_attribute 和 class_inheritable_reader 宏。您可能想知道为什么模型不使用简单的继承方法。原因很简单:模型具有自己的继承层次结构。这些宏允许模型将这些属性投影到目标类中 —— 本例中为 Nonprofit。其中最重要的属性是state_column 以及一系列包含状态、事件和转换的转换表。现在让我们添加形成 DSL 的类方法。

添加类和实例方法

在清单 6 中,终于看到了魔术般地引入了 DSL:


清单 6. acts_as_state_machine 的类方法

				
module ClassMethods
  def states
    read_inheritable_attribute(:states).keys
  end
  
  def event(event, opts={}, &block)
    tt = read_inheritable_attribute(:transition_table)
    
    et = read_inheritable_attribute(:event_table)
    e = et[event.to_sym] = SupportingClasses::Event.new(event, opts, tt, &block)
    define_method("#{event.to_s}!") { e.fire(self) }
  end
  
  def state(name, opts={})
    state = SupportingClasses::State.new(name.to_sym, opts)
    read_inheritable_attribute(:states)[name.to_sym] = state
  
    define_method("#{state.name}?") { current_state == state.name }
  end
  ...


event 和 state 宏是名副其实的简单方法,在 ClassMethods 模块中进行了定义。event 方法读取转换表的属性,然后读取事件表属性。该方法将事件添加到事件表中,然后为事件动态定义方法,将新方法连接到 event 上的 fire 方法。

event 方法之后,模块定义 state 方法。该方法读取状态表并添加新的状态。然后,它将一个方便的方法添加到目标类中,如果实例位于当前状态则返回 true。比如,如果状态标记是 submitted 的话,nonprofit.submitted? 将返回 true。现在,DSL 得到了完全支持。

实例方法工作起来与类方法十分相似。清单 7 展示了实例方法:


清单 7. acts_as_state_machine 的实例方法

				
module InstanceMethods
  def set_initial_state
    write_attribute self.class.state_column, self.class.initial_state.to_s
  end

  ...
  
  def current_state
    self.send(self.class.state_column).to_sym
  end
  
  ...
end


ActMacro 打开类并添加它们。不需要通过 read_inheritable_attribute 宏来使用属性,因为这是 ActiveRecord 定义的类实例变量。我只展示了一个方法,该方法设置初始状态并返回当前状态。其余的方法与此相同。

清单 7 中的第一个方法设置了初始状态,更新已有的 ActiveRecord 列。回顾一下,我在调用 ActMacro 时设置了列名。current_state 方法仅返回实例变量的值。send 方法调用由一个符号参数命名的方法,在本例中其名称是 state_column

结束语

您可能会想,仅仅构建一个状态机并将它当作库来使用岂不更加简单。与之相比,acts_as 插件更加好用。它使您可以有效地将一个状态机列添加到数据库中。其他插件则可以让您完成版本化、构建审计记录、处理图像以及执行大量其他的简单任务,就如同这些任务是 Rails 环境和数据库间的无缝集成一样。

您可能使用过 Java 语言来将 Eclipse 插件、Ant 任务或者 Spring 库集成到您的代码库中,或者使用过 Java 语言引入 EJB 组件。Java 社区的很多理念改变了开发人员对扩展的认识。这篇有关 Rails 的 acts_as 插件的简短介绍展示了一种新的认识方法。Ruby 语言的灵活性改变了我对扩展的认识。acts_as 插件允许新一代开发人员尝试自己编写扩展,这将为 Rails 带来新的扩展浪潮,通过面向方面编程或字节码增强,很多这种技术也可为 Java 开发人员所用。

下一次,我将就使用 Ruby 与利用我在 Java 平台方面的经验解决棘手问题进行深入比较,并以此结束本系列。在那之前,请继续跨越边界。

 

分享到:
评论

相关推荐

    rails_admin_acts_as_list:rails_admin插件以对记录进行排序

    介绍插件,用于对记录进行排序(使用 gem)安装要启用rails_admin_acts_as_list,请将以下内容添加到您的Gemfile : gem 'rails_admin_acts_as_list'gem 'rails_admin' 重要提示: rails_admin_acts_as_list之前必须...

    acts_as_category:想想acts_as_tree +权限

    #ActsAsCategory acts_as_category (Version 2.0 beta)acts_as_category,是acts_as插件在acts_as_tree风格的Ruby on Rails的ActiveRecord的模式,但有一些额外的功能,以及多种便捷视图助手。例子(有关实例方法和...

    acts_as_liked:向任何 Active Record 模型添加类似功能

    $ rails generate acts_as_liked 并且不要忘记迁移您的数据库 $ rake db:migrate 用法 可爱的模特 将acts_as_likeable添加到任何模型,它的实例可以被其他模型喜欢。 class Post &lt; ActiveRecord :: Base ...

    actions_as_commentable:ActiveRecord acts_as_commentable插件

    gem 'acts_as_commentable' Rails gem 'acts_as_commentable' , '3.0.1' Rails gem 'acts_as_commentable' , git: 'git@github.com:jackdempsey/acts_as_commentable.git' , branch: '2.x' 生成器 Rails 3+ ...

    acts_as_paranoid:ActiveRecord插件可让您隐藏和还原记录,而无需实际删除它们

    使徒行传 一个Rails插件来添加软删除。...用法安装gem: gem 'acts_as_paranoid' , '~&gt; 0.7.0' bundle install创建迁移bin/rails generate migration AddDeletedAtToParanoiac deleted_at:datetime:index启用ActsAs

    acts_as_commentable_with_threading:类似于acts_as_commentable; 然而,利用 awesome_nested_set 提供线程评论

    作为可评论行为(现在有评论线程(TM)!!!——在(TM)上开玩笑) 允许将线程注释添加到多个不同的模型。 与acts_as_commentable 兼容(但需要更改数据库架构) ... rails generate acts_as_commentable_with_

    acts_as_aliased:扩展 ActiveRecord

    rails generate acts_as_aliased:install rake db:migrate 这将创建一个新表aliases 。 用法 假设您有一个需要别名的模型Company ,因为公司名称有不同的版本。 使用acts_as_aliased在模型中启用别名: model ...

    跨越边界:Rails迁移

    Ruby on Rails是不断发展的Web开发框架,它实现了一些先进的想法,例如通过配置进行约定、大量的元编程、特定于域的语言以及用数据库包装代替对象关系映射。这篇文章研究的Rails模式迁移是一种把每个数据库的模式...

    acts_as_restful_list:就像acts_as_list一样,但很安静

    acts_as_restful_list 就像acts_as_list 一样,但不必使用非标准方法调用(如insert_at)来弄乱您的代码,acts_as_restful_list 使管理列表变得简单。 您可以像更新其他任何内容一样更新 position 属性,其余的都...

    跨越边界:REST on Rails

    Rails对Web服务提供了优秀的支持。本文介绍Rails中的Web服务,重点放在一个名为Representational State Transfer (REST)的策略上。本文介绍了如何在Ruby on Rails中添加REST风格的Web服务,并从Ruby和Java代码调用...

    acts_as_nps_rateable:Rails 4.x的一个宝石Ruby,实现了Net Promoter Score

    act_as_nps_rateable为基于ActiveRecord的模型提供净促销值(NPS)评级和分析。 促销员净额是衡量客户满意度的指标; 它是会推荐您的产品/服务的客户比例与不会推荐您的产品/服务的客户比例之比。 NPS在Wikipedia...

    acts_as_favoritable:该插件提供了一种跟踪用户收藏夹的简单方法

    $ rails generate acts_as_favoritable:migration $ rake db:migrate 用法 有利模式 贡献 叉它 创建功能分支( git checkout -b my-new-feature ) 提交更改( git commit -am 'Add some feature' ) 推送到分支...

    Secode_level_cache.zip

    Rails的ActiveRecord自身没有带强大的对象缓存功能,这是AR的一个重大的遗憾。早在2008年开始,我们就借鉴了Java强大的ORM 框架... acts_as_cached(:version =&gt; 1, :expires_in =&gt; 1.week) end 标签:Secode

    acts_as_privilege.rb:Rails 的简单权限解决方案

    Acts as privilege 是 Ruby on Rails 的一个插件,它提供了将控制器操作限制为特权资源的功能。 这种基于 ACL 的安全模型被设计为基于角色的访问控制,其中每个角色可以是一组用户。 地位 哲学 只做一件事的通用库...

    acts_as_owner.rb:Rails 的简单所有权解决方案

    Acts as owner 是 Ruby on Rails 的一个插件,它为所有者对象提供了自我查询可拥有对象的所有权的能力。 可拥有对象可以是属于所有者对象的任何对象和属于可拥有对象的任何对象。 属于拥有的可拥有的父代的任何可...

    偏执狂:Rails 3、4和5的acts_as_paranoid

    偏执狂通过destroy记录时将当前时间设置为deleted_at来做到这一点,并通过对模型上所有查询的作用域进行隐藏以仅包括不包含deleted_at字段的记录来将其deleted_at 。 如果您希望实际销毁某个对象,则可以调用...

    actions_as_user:一个可在Rails应用程序上处理多种类型用户的gem

    充当用户 充当用户处理Rails应用程序上的多个用户角色。 它使用多态关联来关联其他模型,并且行为类似于用户。 入门 ActsAsUser 1.2.1可以从Rails 3开始使用。 您可以使用以下命令将其添加... rails g acts_as_user U

    Api-acts_as_api.zip

    Api-acts_as_api.zip,使在rails中创建api响应变得简单和有趣,一个api可以被认为是多个软件设备之间通信的指导手册。例如,api可用于web应用程序之间的数据库通信。通过提取实现并将数据放弃到对象中,api简化了编程...

    acts_as_shopping_cart:简单的购物车实施

    acts_as_shopping_cart 一个简单的购物车实现。 您可以找到示例应用程序。 安装 滑轨3 从0.2.0版开始,不再支持Rails 3。 如果您仍需要在Rails 3应用程序中实现此gem,请使用0-1-x分支 将其包含在您的Gemfile中 ...

    todds_blog:Basic Rails博客:man_technologist::railway_track::writing_hand:

    持续移动且不破坏事物 ‍:factory: :building_construction: :construction: :construction_worker: 正在建设中-请稍后再回来!

Global site tag (gtag.js) - Google Analytics