原文地址:
A taxonomy of Rails plugins
Rails开发人员的一个最大的障碍是学习写插件的基础原则。Ruby的动态性和它所提供的代码重用的多种技术让写插件更复杂。
幸运的是,如果你可以写Rails程序,简单的按照一些基本的模式你也可以写插件。
本文的目的就是使用流行插件常用模式的例子来揭开写插件的神秘面纱。
为什么写插件?
写插件可以:
1,让代码分享更有效,无论是在不同的项目间抑或在同一项目里
2,允许你发布通用代码给社区
3,通过测试一次和重用多次来节省时间和增加你的信心
4,以一个健壮的方式分享功能性,特别是在ActiveRecord里使用名字空间时
使用
管理
Rails通过script/plugin install提供安装脚本,以及创建新插件的generator: script/generate plugin。它们将与URL一起工作,
当尝试插件时会节省时间。你可以在
Rails wiki阅读关于安装和管理插件的更多信息。
创建
Rails提供一个generator来创建插件。执行如下命令:
script/generate plugin acts_as_
这将在vendor/plugins/acts_as_目录生成一些文件,给你一些存根来让你填充。你将注意到生成一些用来安装、卸载和测试的文件
以及一些rake任务。许多插件不需要引入rake任务,并且安装和卸载插件时你可以做任何事情:也许你想让你的插件引入数据库表
(例如taggable插件需要数据库表来存储tags),或者你可能想在script/plugin install时检查依赖。
Rubyisms
下面Ruby提供的任一工具和技术都被插件用来扩展功能性:
1,Mixins:使用模块来引入或扩展类
2,打开一个类或模块定义并且添加或覆盖方法
3,通过回调和钩子来动态扩展:method_missing,Class#inherited,Module#const_missing,Module#included
4,通过代码生成来动态扩展:eval,class_eval,instance_eval
这些技术形成两种分类:
1,使用模块和类来扩展已有的类和提供新特性
2,使用自省来让一般代码适用于特殊情况
当写一个插件时考虑什么需要被扩展是很重要的。如果需要使用复杂的元编程来让你的插件适用于应用程序,则需要注意确保
同步不会产生不预期的结果。
扩展ActiveRecord
扩展ActiveRecord类的最流行的技术是通过使用模块和一个acts_as_类方法。大部分插件将它们自己分成包含类方法和实例方法
的两个模块,这让方法将在哪里使用很清晰。
添加新代码到ActiveRecord
为了使用你的新的acts_as_插件来扩展ActiveRecord::Base,最常见的技术是简单的添加如下内容到插件的init.rb文件:
ActiveRecord::Base.send(:include, MyPlugin)
你也可以使用class_eval来达到同样的效果。下面的例子来自
acts_as_rateable:
ActiveRecord::Base.class_eval do
include FortiusOne::Acts::Rateable
end
注意使用公司名来为作者的插件创建一个名字空间。
下一步是使用持有acts_as_方法的模块来扩展ActiveRecord::Base。下面也是来自acts_as_rateable:
module FortiusOne
module Acts
module Rateable
def self.included(mod)
mod.extend(ClassMethods)
end
module ClassMethods
def acts_as_rateable(options = {})
end
end
这样,任何ActiveRecord类都可以在类定义里使用acts_as_rateable。这样做将把提供的任何options发送到插件的ClassMethods
模块的acts_as_rateable方法里。
管理options
大部分插件在它们的acts_as_方法里使用extend和include来给接受的类提供类方法和实例方法。这也是options被处理的地方。
Options通常通过使用哈希来处理:
def acts_as_taggable(options = {})
如果options被类方法和实例方法同时使用,则write_inheritable_attribute和class_inheritable_reader可以用来节省设置。
下面是一个修改过的
acts_as_state_machine:
def acts_as_state_machine(opts)
self.extend(ClassMethods)
raise NoInitialState unless opts[:initial]
write_inheritable_attribute :initial_state, opts[:initial]
class_inheritable_reader :initial_state
如此多的插件使用write_inheritable_attribute而不是cattr_方法有一个原因。考虑如果initial_state是一个类变量会发生什
么:其他使用该插件的对象将包含对同样的initial_state的引用。插件一般为不同的、可重用的代码,所以initial_state的一
个副本(而不是一个引用)是需要的。
重用一套关联和回调
acts_as_taggable_on_steroids阐述了插件的一个简单使用:
module ClassMethods
def acts_as_taggable(options = {})
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
has_many :tags, :through => :taggings
before_save :save_cached_tag_list
after_save :save_tags
你可以看到,作者从插件的类方法里调用关联和回调。这是可以的,因为acts_as_taggable方法运行于接受类的定义里。
我发现我自己使用同样的技术来定义复杂而重用的关联(特别是重用多态has_many关联的情况)。
例如,我在一个基于组的使用回调和多态has_many关联的安全系统上工作。我写了一个插件来替代添加代码到每个需要基于组
的安全的类中。比较一下下面的代码:
class Contact < ActiveRecord::Base
acts_as_restricted
end
原始代码:
class Contact < ActiveRecord::Base
has_many :restrictions, :as => :restricted, :dependent => :destroy, :extend => ...
before_save :auto_groups
after_create :save_groups
...
该代码可以在我所有的项目间重用了,并且新的类可以“限制为”仅仅添加acts_as_restricted到类定义中。
这可以帮助节省时间--特别是当客户仍在犹豫什么应该被限制时。
结构
我宁愿通过为每个模块创建单独的文件来组织我的插件结构。这在
acts_as_solr中阐述了,它为类方法使用一个单独的文件。
模块可以被类方法,实例方法和singleton方法使用。尽管可能看起来区分类方法和singleton方法很难,有些插件作者使用
这来阐明他们的代码。既然singleton方法适用于特殊的对象而不是一些对象,你可以根据需要高效的添加新方法到一个类的
特殊实例。
或者,对于acts_as_rateable,SingletonMethods模块用来区分添加到ActiveRecord的方法和那些添加到接受者的方法。
在这里finders被添加。
扩展ActionController和ActionView
ActiveRecord插件使用的同样的技术被用来扩展ActionController和ActionView。UploadProgress插件扩展了ActionView和
ActionController来添加它自己的helpers和controller功能性:
ActionController::Base.send(:include, UploadProgress)
ActionView::Base.send(:include, UploadProgress::UPloadProgressHelper)
这样Controllers可以通过调用定义在UploadProgress::ClassMethods中的upload_status_for来使用插件。
最佳实践
1,为良好设计的插件避开对include的任意使用
2,仔细的结构化你的插件来确保它们很容易理解和测试
3,确保你使用write_inheritable_attribute和class_inheritable_reader这样你不会以引用设置而不是创建新的告终
4,将写测试作为设计过程中的一部分,不仅是因为插件可能写起来慢,而且当使用mongrel/webrick时它们不能重新加载
5,确保生成的SQL不会导致注射攻击:escape用户输入(我已经找到了生成原始SQL而不用escape变量的开源插件)
6,仔细考虑元编程代码的隐含意义,对eval和eval-like方法保持警惕。
下一步
本文将使用插件被如何测试的例子来继续。
引用
acts_as_rateable
acts_as_state_machine
acts_as_taggable_on_steroids
acts_as_solr
Rails Plugin Directory
Rails Wiki on plugins