Ruby的哲学基于一个强大的原始程序,即程序员的幸福。Ruby坚信程序员的快乐,并提供了许多不同的方法来实现它。它的元编程功能允许程序员在运行时编写动态代码。它的线程功能为程序员提供了编写多线程代码的优雅方式。它的钩子方法帮助程序员在运行时扩展程序的行为。

上述功能以及其他一些很酷的语言方面使Ruby成为编写代码的首选选择之一。这篇文章将探讨Ruby中一些重要的钩子方法。我们将讨论钩子方法的不同方面,例如它们是什么,它们用于什么,以及我们如何使用它们来解决不同的问题。我们还将看看流行的Ruby框架/ gems /库如何使用它们来提供非常酷的功能。

让我们开始吧。

什么是钩子方法?

Hook方法提供了一种在运行时扩展程序行为的方法。想象一下,只要子类继承某个特定的父类或者优雅地处理对象上的非可调用方法而不允许编译器引发异常,就能够得到通知。这些是钩子方法的一些用例,但它们的用法不仅限于此。不同的框架/库使用不同的钩子方法来实现其所需的功能。

我们将在这篇文章中讨论以下钩子方法:

  • included
  • extended
  • prepended
  • inherited
  • method_missing

included

Ruby为我们提供了一种使用modules(在其他语言中叫做mixins)编写模块化代码的方法,以后可以在其他modules/classes
中使用。module非常简单,它是一个可以在其他地方使用的独立代码。

例如,如果我们想编写一些代码,只要调用特定方法,它就会返回一个静态字符串。我们称之为那种方法name。您可能也希望在其他位置使用同一段代码。module在这里创造一个完美的意义。让我们创建一个:

module Person
  def name
    puts "My name is Person"
  end
end

这是一个非常简单的模块,只有一个方法name返回一个静态字符串。我们在程序中使用它:

class User
  include Person
end

Ruby提供了一些不同的使用方法modules。其中之一是includeinclude
做了什么使在底层module
中定义的方法可以被class
的实例调用呢。在我们的例子中,Person模块中定义的方法可用作User类对象的实例方法。就好像我们已经在User类本身中编写了该方法name,但在module中定义它的优点是可重用性。要调用name我们需要创建一个User实例对象,然后调用name新创建的对象。例如:

User.new.name 
=> My name is Person

我们来看看基于的钩子方法includeincluded 是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 钩子的。

Devise中的 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钩子来提供它们。很简约。

extended

Ruby还允许开发人员使用extend一个模块,这个模块与include
有点不同。不是将定义的方法应用于module类的实例,而是将方法extend应用于类本身。我们来看一个简单的例子:

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 是如何使用它的。

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 是如何使用上述方法为我们提供可用功能的。

prepended

还有另一种方法可以使用在模块中定义的方法prependprepend在Ruby中2.0中引入,它是由完全不同的includeextend。通过所使用的方法includeextend可以通过在目标模块/类中定义的方法来覆盖。例如,如果我们name在某个模块中定义了一个方法,并且在目标模块/类中定义了相同的方法,那么我们类中定义的方法name将从模块中重写。prepend是完全不同的,因为它覆盖了我们的模块/类中定义的方法与prepending模块中定义的方法。我们来看一个简单的例子:

module Person
  def name
    "My name belongs to Person"
  end
end

class User
  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版本。

inherited

继承是面向对象编程中最重要的概念之一。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

正如您所看到的,inherited每当Person类被某个子类继承时,都会调用类方法。运行上面的代码段显示:

User inherits Person
My name is Person

让我们看看它的代码库Rails是如何使用的inherited

Rails中的 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

method_missing可能是使用最广泛的Ruby钩子。它可以在许多流行的Ruby框架/ gem / library中找到。当我们的代码试图在一个对象上调用一个不存在的方法时调用它。我们来看一个简单的例子:

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,并调用两个方法,nameaddress分别。由于name定义Person,它将运行顺利。但是address没有定义Person,并将引发异常。该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 的。

Rake中的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钩子,以及你使用它们所解决的问题。

原文:https://www.sitepoint.com/rubys-important-hook-methods/

2条评论 顺序楼层
请先登录再回复