原文地址: http://www.sitepoint.com/rubys-important-hook-methods/
Ruby 的哲学理念是基于一个基本的要素,那就是让程序员快乐。Ruby 非常注重程序员的快乐,并且也提供了许多不同的方法来实现它。 它的元编程能力能够让程序员编写在运行时动态生成的代码。它的线程功能使得程序员有一种优雅的的方式编写多线程代码。 它的钩子方法能让程序员在程序运行时扩展它的行为。
上述的这些特性,以及一些其他很酷的语言方面,使得 Ruby 成为编写代码的优先选择之一。 本文将探讨 Ruby 中的一些重要的钩子方法。我们将从不同方面讨论钩子方法,如它们是什么,它们用于什么,以及我们如何使用它们来解决不同的问题。 我们同时也了解一下一些流行的 Ruby 框架/Gem 包/库是如何使用它们来提供非常酷的特性的。
我们开始吧。
钩子方法提供了一种方式用于在程序运行时扩展程序的行为。 假设有这样的功能,可以在无论何时一个子类继承了一些特定的父类时收到通知, 或者是比较优雅地处理一个对象上的不可调用的方法而不是让编译器抛出异常。 这些情况就是使用钩子方法,但是它们的用法并不仅限于此。 不同的框架/库使用了不同的钩子方法来实现它们的功能。
在本文中我们将会讨论如下几个钩子方法:
Ruby 给我们提供了一种方式使用 模块(modules)
(在其他语言中被称作 混入类(mixins)
)来编写模块化的代码供其他的 模块
/类
使用。
模块
的概念很简单,它就是一个可以在其他地方使用的独立代码块。
例如,如果我们想要编写一些代码在任何时候调用特定的方法都会返回一个静态字符串。
我们姑且将这个方法称作 name
。你可能在其他地方也会想使用同一块代码。
这样最好是新建一个模块。让我们来创建一个:
module Person
def name
puts "My name is Person"
end
end
这是一个非常简单的模块,仅有一个 name
方法用于返回一个静态字符串。在我们的程序中使用这个模块:
class User
include Person
end
Ruby 提供了一些不同的方法来使用模块
。include
是其中之一。include
所做的就是将在 module
内定义的方法在一个 class
的实例变量上可用。
在我们的例子中,是将 Person
模块中定义的方法变为一个 User
类实例对象的方法。
这就相当于我们是将 name
方法写在 User
类里一样,但是定义在 module
里的好处是可复用。
要调用 name
方法我们需要创建一个 User
的实例对象,然后再在这个对象上调用 name
方法。例如:
User.new.name
=> My name is Person
让我们看看基于 include
的钩子方法。included
是 Ruby 提供的一个钩子方法,当你在一些 module
或者 class
中 include
了一个 module
时它会被调用。
更新 Person
模块:
module Person
def self.included(base)
puts "#{base} included #{self}"
end
def name
"My name is Person"
end
end
你可以看到一个新的方法 included
被定义为 Person
模块的类方法。当你在其他的模块或者类中执行 include Person
时,这个 included
方法会被调用。
该方法接收的一个参数是对包含该模块的类的引用。试试运行 User.new.name
,你会看到如下的输出:
User included Person
My name is Person
正如你所见,base
返回的是包含该模块的类名。现在我们有了一个包含 Person
模块的类的引用,我们可以通过元编程来实现我们想要的功能。
让我们来看看 Devise是如何使用 included
钩子的。
included
Devise 是 Ruby 中使用最广泛的身份验证 gem 包之一。它主要是由我喜欢的程序员 José Valim 开发的,现在是由一些了不起的贡献者在维护。 Devise 为我们提供了从注册到登录,从忘记密码到找回密码等等完善的功能。它可以让我们在用户模型中使用简单的语法来配置各种模块:
devise :database_authenticatable, :registerable, :validatable
在我们模型中使用的 devise
方法在这里定义。
为了方便我将这段代码粘贴在下面:
def devise(*modules)
options = modules.extract_options!.dup
selected_modules = modules.map(&:to_sym).uniq.sort_by do |s|
Devise::ALL.index(s) || -1 # follow Devise::ALL order
end
devise_modules_hook! do
include Devise::Models::Authenticatable
selected_modules.each do |m|
mod = Devise::Models.const_get(m.to_s.classify)
if mod.const_defined?("ClassMethods")
class_mod = mod.const_get("ClassMethods")
extend class_mod
if class_mod.respond_to?(:available_configs)
available_configs = class_mod.available_configs
available_configs.each do |config|
next unless options.key?(config)
send(:"#{config}=", options.delete(config))
end
end
end
include mod
end
self.devise_modules |= selected_modules
options.each { |key, value| send(:"#{key}=", value) }
end
end
在我们的模型中传给 devise
方法的模块名将会作为一个数组保存在 *modules
中。
对于传入的模块调用 extract_options!
方法提取可能传入的选项。
在 11 行中调用 each
方法,并且每个模块在代码块中用 m
表示。
在 12 行中 m
将会转化为一个常量(类名),因此使用 m.to.classify
一个例如 :validatable
这样的符号会变为 Validatable
。
随便说一下 classify
是 ActiveSupport 的方法。
Devise::Models.const_get(m.to_classify)
会获取该模块的引用,并赋值给 mod
。
在 27 行使用 include mod
包含该模块。
例子中的 Validatable
模块是定义在这里。
Validatable
的 included
钩子方法定义如下:
def self.included(base)
base.extend ClassMethods
assert_validations_api!(base)
base.class_eval do
validates_presence_of :email, if: :email_required?
validates_uniqueness_of :email, allow_blank: true, if: :email_changed?
validates_format_of :email, with: email_regexp, allow_blank: true, if: :email_changed?
validates_presence_of :password, if: :password_required?
validates_confirmation_of :password, if: :password_required?
validates_length_of :password, within: password_length, allow_blank: true
end
end
此时模型是 base
。在第 5 行的 class_eval
代码块会以该类作为上下文进行求值运算。
通过 class_eval
编写的代码与直接打开该类的文件将代码粘贴进去效果是一样的。
Devise 是通过 class_eval
将验证包含到我们的用户模型中的。
当我们试着使用 Devise 注册或者登录时,我们会看到这些验证,但是我们并没有编写这些验证代码。
Devise 是利用了 included
钩子来实现这些的。非常的优雅吧。
Ruby 也允许开发者 扩展(extend)
一个模块,这与 包含(include)
有点不同。
extend
是将定义在 模块(module)
内的方法应用为类的方法,而不是实例的方法。
让我们来看一个简单的例子:
module Person
def name
"My name is Person"
end
end
class User
extend Person
end
puts User.name # => My name is Person
正如你所看到的,我们将 Person
模块内定义的 name
方法作为了 User
的类方法调用。
extend
将 Person
模块内的方法添加到了 User
类中。extend
同样也可以用于将模块内的方法作为单例方法 (singleton methods)。
让我们再来看另外一个例子:
# We are using same Person module and User class from previous example.
u1 = User.new
u2 = User.new
u1.extend Person
puts u1.name # => My name is Person
puts u2.name # => undefined method `name' for #<User:0x007fb8aaa2ab38> (NoMethodError)
我们创建了两个 User
的实例对象,并将 Person
作为参数在 u1
上调用 extend
方法。
使用这种调用方式,Person
的 name
方法仅对 u1
有效,对于其他实例是无效的。
正如 included
一样,与 extend
相对应的钩子方法是 extended
。
当一个模块被其他模块或者类执行了 extend
操作时,该方法将会被调用。
让我们来看一个例子:
# Modified version of Person module
module Person
def self.extended(base)
puts "#{base} extended #{self}"
end
def name
"My name is Person"
end
end
class User
extend Person
end
该代码的运行结果是输出 User extended Person
。
关于 extended
的介绍已经完了,让我们来看看 ActiveRecord
是如何使用它的。
extended
ActiveRecord
是在 Ruby 以及 Rails 中广泛使用的 ORM 框架。它具有许多酷的特性,
因此使用它在很多情况下成为了 ORM 的首选。让我们进入 ActiveRecord
内部看看 ActiveRecord
是如何使用回调的。
(我们使用的是 Rails v3.2.21)
ActiveRecord
在这里 extend
了 ActiveRecord::Models
模块。
extend ActiveModel::Callbacks
ActiveModel
提供了一套在模型类中使用的接口。它们允许 ActionPack 与不是 ActiveRecord 的模型进行交互。
在这里, ActiveModel::Callbacks
内部你将会看到如下代码:
def self.extended(base)
base.class_eval do
include ActiveSupport::Callbacks
end
end
ActiveModel::Callbacks
对 base
即就是 ActiveRecord::Callbacks
调用了 class_eval
方法,
并包含了 ActiveSupport::Callbacks
模块。我们前面已经提到过了,对一个类调用 class_eval
与手动地将代码写在这个类里是一样的。
ActiveSupport::Callbacks
为 ActiveRecord::Callbacks
提供了 Rails 中的回调方法。
这里我们讨论了 extend
方法,以及与之对应的钩子 extended
。并且也了解了 ActiveRecord
/ ActiveModel
是如何使用上述方法为我们提供可用功能的。
另一个使用定义在模块内部方法的方式称为 prepend
。prepend
是在 Ruby 2.0 中引入的,并且与 include
和 extend
很不一样。
使用 include
和 extend
引入的方法可以被目标模块/类重新定义覆盖。
例如,如果我们在某个模块中定义了一个名为 name
的方法,并且在目标模块/类中也定义同名的方法。
那么这个在我们类在定义的 name
方法将会覆盖模块中的。而 prepend
是不一样的,它会将 prepend
引入的模块
中的方法覆盖掉我们模块/类中定义的方法。让我们来看一个简单的例子:
module Person
def name
"My name belongs to Person"
end
end
class User
include Person
def name
"My name belongs to User"
end
end
puts User.new.name
=> My name belongs to User
现在再来看看 prepend
的情况:
module Person
def name
"My name belongs to Person"
end
end
class User
prepend Person
def name
"My name belongs to User"
end
end
puts User.new.name
=> My name belongs to Person
使用 prepend Person
会将 User
中的同名方法给覆盖掉,因此在终端输出的结果为 My name belongs to Person
。
prepend
实际上是将方法添加到方法链的前端。在调用 User
类内定义的 name
方法时,会调用 super
从而调用 Person
模块的 name
。
与 prepend
对应的回调名为(你应该猜到了) prepended
。当一个模块被预置到另一个模块/类中时它会被调用。
我们来看下效果。更新 Person
模块的定义:
module Person
def self.prepended(base)
puts "#{self} prepended to #{base}"
end
def name
"My name belongs to Person"
end
end
你再运行这段代码应该会看到如下结果:
Person prepended to User
My name belongs to Person
prepend
的引入是为了去除 alias_method_chain
hack 的丑陋,它曾被 Rails 以及其他库广泛地使用以达到与 prepend
相同的功能。
因为 prepend
只有在 Ruby >= 2.0 的版本中才能使用,因此如果你打算使用 prepend
的话,那么你就应该升级你的 Ruby 版本。
继承是面向对象中一个最重要的概念。Ruby 是一门面向对象的编程语言,并且提供了从基/父类继承一个子类的功能。 我们来看一个简单的例子:
class Person
def name
"My name is Person"
end
end
class User < Person
end
puts User.new.name # => My name is Person
我们创建了一个 Person
类和一个子类 User
。在 Person
中定义的方法也成为了 User
的一部分。
这是非常简单的继承。你可能会好奇,是否有什么方法可以在一个类被其他类继承时收到通知呢?
是的,Ruby 有一个名为 inherited
的钩子可以实现。我们再看看这个例子:
class Person
def self.inherited(child_class)
puts "#{child_class} inherits #{self}"
end
def name
"My name is Person"
end
end
class User < Person
end
puts User.new.name
正如你所见,当 Person
类被其他子类继承时 inherited
类方法将会被调用。
运行以上代码结果如下:
User inherits Person
My name is Person
让我们看看 Rails
在它的代码中是如何使用 inherited
的。
inherited
Rails 应用中有一个重要的类名为 Application
,定义中 config/application.rb 文件内。
这个类执行了许多不同的任务,如运行所有的 Railties,引擎以及插件的初始化。
关于 Application
类的一个有趣的事件是,在同一个进程中不能运行两个实例。
如果我们尝试修改这个行为,Rails 将会抛出一个异常。让我们来看看 Rails 是如何实现这个特性的。
Application
类继承自 Rails::Application
,它是在这里定义的。
在 62 行定义了 inherited
钩子,它会在我们的 Rails 应用 Application
类继承 Rails::Application
时被调用。
inherited
钩子的代码如下:
class << self
def inherited(base)
raise "You cannot have more than one Rails::Application" if Rails.application
super
Rails.application = base.instance
Rails.application.add_lib_to_load_path!
ActiveSupport.run_load_hooks(:before_configuration, base.instance)
end
end
class << self
是 Ruby 中的另一个定义类方法的方式。在 inherited
中的第 1 行是检查 Rails.application
是否已存在。
如果存在则抛出异常。第一次运行这段代码时 Rails.application
会返回 false 然后调用 super
。
在这里 super
即是 Rails::Engine
的 inherited
钩子,因为 Rails::Application
继承自 Rails::Engine
。
在下一行,你会看到 Rails.application
被赋值为 base.instance
。其余就是设置 Rails 应用了。
这就是 Rails 如何巧妙地使用 inherited
钩子来实现我们的 Rails Application
类的单实例。
method_missing
可能是 Ruby 中使用最广的钩子。在许多流行的 Ruby 框架/gem 包/库中都有使用它。
当我们试图访问一个对象上不存在的方法时则会调用这个钩子方法。
让我们来看一个简单的例子:
class Person
def name
"My name is Person"
end
end
p = Person.new
puts p.name # => My name is Person
puts p.address # => undefined method `address' for #<Person:0x007fb730a2b450> (NoMethodError)
我们定义了一个简单的 Person
类,它只有一个 name
方法。然后创建一个 Person
的实例对象,
并分别调用 name
和 address
两个方法。因为 Person
中定义了 name
,因此这个运行没问题。
然而 Person
并没有定义 address
,这将会抛出一个异常。
method_missing
钩子可以优雅地捕捉到这些未定义的方法,避免此类异常。
让我们修改一下 Person
类:
class Person
def method_missing(sym, *args)
"#{sym} not defined on #{self}"
end
def name
"My name is Person"
end
end
p = Person.new
puts p.name # => My name is Person
puts p.address # => address not defined on #<Person:0x007fb2bb022fe0>
method_missing
接收两个参数:被调用的方法名和传递给该方法的参数。
首先 Ruby 会寻找我们试图调用的方法,如果方法没找到则会寻找 method_missing
方法。
现在我们重载了 Person
中的 method_missing
,因此 Ruby 将会调用它而不是抛出异常。
让我们来看看 Rake
是如何使用 method_missing
的。
method_missing
Rake
是 Ruby 中使用最广泛的 gem 包之一。Rake
使用 method_missing
来提供访问传递给 Rake 任务的参数。
首先创建一个简单的 rake 任务:
task :hello do
puts "Hello"
end
如果你通过调用 rake hello
来执行这个任务,你会看到输出 Hello
。
让我们扩展这个 rake 任务,以便接收一个参数(一个人名)并向他打招呼:
task :hello, :name do |t, args|
puts "Hello #{args.name}"
end
t
是任务名,args
保存了传递过来的参数。正如你所见,我们调用 args.name
来获取传递给 hello
任务的 name
参数。
运行该任务,并传递一个参数:
rake hello["Imran Latif"]
=> Hello Imran Latif
让我们来看看 Rake
是如何使用 method_missing
为我们提供了传递给任务的参数的。
在上面任务中的 args
对象是一个 Rake::TaskArguments
实例,它是在这里所定义。
这个类负责管理传递给 Rake 任务的参数。查看 Rake::TaskArguments
的代码,你会发现并没有定义相关的方法将参数传给任务。
那么 Rake
是如何将参数提供给任务的呢?答案是 Rake
是使用了 method_missing
巧妙地实现了这个功能。
看看第 64 行 method_missing
的定义:
def method_missing(sym, *args)
lookup(sym.to_sym)
end
在这个类中定义 method_missing
是为了保证能够访问到那些未定义的方法,而不是由 Ruby 抛出异常。
在 method_missing
中它调用了 lookup
方法:
def lookup(name)
if @hash.has_key?(name)
@hash[name]
elsif @parent
@parent.lookup(name)
end
end
method_missing
调用 lookup
,并将方法名以 Symbol(符号)
的形式传递给它。
lookup
方法将会在 @hash
中进行查找,它是在 Rake::TaskArguments
的构造函数中创建的。
如果 @hash
中包含该参数则返回,如果在 @hash
中没有则 Rake
会尝试调用 @parent
的 lookup
。
如果该参数没有找到,则什么都不返回。
这就是 Rake
如何巧妙地使用 method_missing
提供了访问传递给 Rake 任务的参数的。
感谢Jim Weirich编写了 Rake。
我们讨论了 5 个重要的 Ruby 钩子方法,探索了它们是如何工作的,以及一些流行的框架/gem 包是如何使用它们来提供一些优雅的功能。 我希望你能喜欢这篇文章。请在评论中告诉我们你所喜欢的 Ruby 钩子,以及你使用它们所解决的问题。