Ruby中的钩子方法详解蒲京娱乐场网站

class Product < ActiveRecord::Base  
  set_table_name ‘produce’   
end 

extend ActiveModel::Callbacks

我最近考虑了很多元编程(Metaprogramming)的问题,并希望看到更多这方面技术的例子和讲解。无论好坏,元编程已经进入Ruby社区,并成为完成各种任务和简化代码的标准方式。既然找不到这类资源,我准备抛砖引玉写一些通用Ruby技术的文章。这些内容可能对从其它语言转向Ruby或者还没有体验到Ruby元编程乐趣的程序员非常有用。

puts u1.name # => My name is Person
puts u2.name # => undefined method `name’ for
#<User:0x007fb8aaa2ab38> (NoMethodError)

x = {‘abc’ => 123}  
x.abc # => 123  
x.foo = :baz 
x # => {‘abc’ => 123, ‘foo’ => :baz} 

  def name
    “My name is Person”
  end
end

复制代码 代码如下:

Application 类继承自
Rails::Application,它是在这里定义的。
在62行定义了 inherited 钩子,它会在我们的Rails应用 Application 类继承
Rails::Application 时被调用。 inherited 钩子的代码如下:

**

正如你所见,当 Person 类被其他子类继承时 inherited 类方法将会被调用。
运行以上代码结果如下:

复制代码 代码如下:

正如你所见,base 返回的是包含该模块的类名。现在我们有了一个包含 Person
模块的类的引用,我们可以通过元编程来实现我们想要的功能。 让我们来看看
Devise是如何使用 included 钩子的。

  除了Struct,还能在SOAP4R和Camping找到轻松创建类的例子。Camping尤其令人感兴趣,因为它有专门的方法创建这些类,被你的controller和view继承。Camping的许多有趣的功能都是用这种方式实现的:

module Person
  def name
    “My name belongs to Person”
  end
end

  希望这些技巧和技术已经为您阐明了元编程。我并不声称自己是Ruby或者元编程方面的专家,这只是我对这个问题的一些想法。

Ruby提供了一些不同的方法来使用模块。include 是其中之一。include
所做的就是将在 module 内定义的方法在一个 class 的实例变量上可用。
在我们的例子中,是将 Person 模块中定义的方法变为一个 User
类实例对象的方法。 这就相当于我们是将 name 方法写在 User
类里一样,但是定义在 module 里的好处是可复用。 要调用 name
方法我们需要创建一个 User 的实例对象,然后再在这个对象上调用 name
方法。例如:

module ActiveRecord  
  class Base  
    def self.set_table_name name  
      define_attr_method :table_name, name  
    end 
    def self.define_attr_method(name, value)  
      singleton_class.send :alias_method, “original_#{name}”,
name  
      singleton_class.class_eval do   
        define_method(name) do     
          value  
        end 
      end 
    end 
  end   
end 

当我们试着使用Devise注册或者登录时,我们会看到这些验证,但是我们并没有编写这些验证代码。
Devise是利用了 included 钩子来实现这些的。非常的优雅吧。

复制代码 代码如下:

ActiveRecord 是在 Ruby 以及 Rails
中广泛使用的ORM框架。它具有许多酷的特性,
因此使用它在很多情况下成为了ORM的首选。让我们进入 ActiveRecord 内部看看
ActiveRecord 是如何使用回调的。 (我们使用的是 Rails v3.2.21)

class Hash 
  def method_missing(m,*a)  
    if m.to_s =~ /=$/  
      self[$`] = a[0]  
    elsif a.empty?    
      self[m]  
    else 
      raise NoMethodError, “#{m}” 
    end 
  end 
end 

与 prepend 对应的回调名为(你应该猜到了)
prepended。当一个模块被预置到另一个模块/类中时它会被调用。
我们来看下效果。更新 Person 模块的定义:

  这个例子展示了编写add_tracing和remove_tracing的一种典型方法。它依赖于第1条的单例类:

User inherits Person
My name is Person

  这非常简单,并且难以置信的强大。可以看一下Test::Unit到处使用这种方法。

Rails应用中有一个重要的类名为 Application ,定义中 config/application.rb
文件内。
这个类执行了许多不同的任务,如运行所有的Railties,引擎以及插件的初始化。
关于 Application 类的一个有趣的事件是,在同一个进程中不能运行两个实例。
如果我们尝试修改这个行为,Rails将会抛出一个异常。让我们来看看Rails是如何实现这个特性的。

  我会在下文使用这个方法。

结束语

  ERb和Rails用这种技术来设置哪些实例变量是有效的。例如:

Ruby的哲学理念是基于一个基本的要素,那就是让程序员快乐。Ruby非常注重程序员的快乐,并且也提供了许多不同的方法来实现它。
它的元编程能力能够让程序员编写在运行时动态生成的代码。它的线程功能使得程序员有一种优雅的的方式编写多线程代码。
它的钩子方法能让程序员在程序运行时扩展它的行为。

6. 替换方法 Replacing methods

class User
  include Person
end

复制代码 代码如下:

另一个使用定义在模块内部方法的方式称为 prepend。prepend 是在Ruby
2.0中引入的,并且与 include 和 extend 很不一样。 使用 include 和 extend
引入的方法可以被目标模块/类重新定义覆盖。
例如,如果我们在某个模块中定义了一个名为 name
的方法,并且在目标模块/类中也定义同名的方法。 那么这个在我们类在定义的
name 方法将会覆盖模块中的。而 prepend 是不一样的,它会将 prepend
引入的模块
中的方法覆盖掉我们模块/类中定义的方法。让我们来看一个简单的例子:

**

module Person
  def name
    “My name is Person”
  end
end

4. 使用method_missing来做有趣的事 Use method_missing to do
interesting things

复制代码 代码如下:

  调用也很容易:
p.call(*args) 

继承是面向对象中一个最重要的概念。Ruby是一门面向对象的编程语言,并且提供了从基/父类继承一个子类的功能。
我们来看一个简单的例子:

  一个极端的用法是临时修改一个方法,然后再还原。例如:

你可以看到一个新的方法 included 被定义为 Person
模块的类方法。当你在其他的模块或者类中执行 include Person 时,这个
included 方法会被调用。
该方法接收的一个参数是对包含该模块的类的引用。试试运行
User.new.name,你会看到如下的输出:

  除了闭包(block),method_missing可能是Ruby最强大的特性,也是最容易滥用的一个。用好method_missing的话有些代码会变得超级简单,甚至是不能缺少。一个好的例子(Camping)是扩展Hash:

puts User.new.name
=> My name belongs to Person

会生成:

class Person
  def name
     “My name is Person”
  end
end

复制代码 代码如下:

复制代码 代码如下:

  这对于无法预测的方法来说可以轻松的达到可扩展性。我最近创建了一个小型验证框架,核心的验证类会找出自身所有以check_开头的方法并调用,这样就可以轻松地增加新的验证:只要往类或实例中添加新方法。
methods.grep /^check_/ do |m|  
  self.send m  
end 

复制代码 代码如下:

  module_eval会在调用的module的上下文中执行字符串或block。这个比较适合在module或单例类中定义新方法。instance_eval和module_eval的主要区别在于定义的方法会放在哪里。如果你用String.instance_eval定义foo方法会得到String.foo,如果是用module_eval会得到String.new.foo。

复制代码 代码如下:

3. 动态创建class和module Create classes and modules dynamically

module Person
  def self.extended(base)
    puts “#{base} extended #{self}”
  end

name = x.nil? ? “default name” : x.name 

复制代码 代码如下:

  如果你确实需要用eval,你可以控制哪些变量是有效的。这时候要用kernel方法binding来获得所绑定的对象。例如:

class Person
  def self.inherited(child_class)
    puts “#{child_class} inherits #{self}”
  end

<body> 
  <h1 class=”header”>Blog</h1> 
  <div class=”content”> 
    Hellu  
  </div> 
</body> 

复制代码 代码如下:

复制代码 代码如下:

这就是 Rake 如何巧妙地使用 method_missing
提供了访问传递给Rake任务的参数的。 感谢Jim
Weirich
编写了Rake。

  如你所见,如果有人调用了一个hash不存在的方法,则会搜索内部集合。如果方法名以=结尾,则会赋给同名的key。

Devise::Models.const_get(m.to_classify) 会获取该模块的引用,并赋值给
mod。 在27行使用 include mod 包含该模块。 例子中的 Validatable
模块是定义在这里。 Validatable 的 included 钩子方法定义如下:

  如果这些方法是添加到module(有一点点不同,看你能不能写出来!),你也可以在类而非实例上添加和删除tracing。

class User < Person
end

1. 使用单例类 Use the singleton-class

def self.included(base)
  base.extend ClassMethods
  assert_validations_api!(base)

  你也可以这样创建module,然后在类中包含module。

  selected_modules = modules.map(&:to_sym).uniq.sort_by do |s|
    Devise::ALL.index(s) || -1  # follow Devise::ALL order
  end

复制代码 代码如下:

    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

  Markaby中可以找到另一个很好的method_missing技巧。以下引用的代码可以生成任何包含CSS
class的XHTML标签:

Devise中的 included

def get_b; binding; end 
foo = 13  
eval(“puts foo”,get_b) # => NameError: undefined local variable or
method `foo’ for main:Object 

我们讨论了5个重要的Ruby钩子方法,探索了它们是如何工作的,以及一些流行的框架/gem包是如何使用它们来提供一些优雅的功能。
我希望你能喜欢这篇文章。请在评论中告诉我们你所喜欢的Ruby钩子,以及你使用它们所解决的问题。

5. 方法模式的调度 Dispatch on method-patterns

复制代码 代码如下:

class String 
  alias_method :original_reverse, :reverse 
  def reverse   
    puts “reversing, please wait…” original_reverse  
  end 
end 

复制代码 代码如下:

  1. 实例变量的内省 Introspect on instance variables**

# Modified version of Person module

您可能感兴趣的文章:

上述的这些特性,以及一些其他很酷的语言方面,使得Ruby成为编写代码的优先选择之一。
本文将探讨Ruby中的一些重要的钩子方法。我们将从不同方面讨论钩子方法,如它们是什么,它们用于什么,以及我们如何使用它们来解决不同的问题。
我们同时也了解一下一些流行的Ruby框架/Gem包/库是如何使用它们来提供非常酷的特性的。

复制代码 代码如下:

def lookup(name)
  if @hash.has_key?(name)
   @hash[name]
  elsif @parent
    @parent.lookup(name)
  end
end

def trace(*mths)  
  add_tracing(*mths) # aliases the methods named, adding
tracing      
  yield 
  remove_tracing(*mths) # removes the tracing aliases  
end 

钩子方法提供了一种方式用于在程序运行时扩展程序的行为。
假设有这样的功能,可以在无论何时一个子类继承了一些特定的父类时收到通知,
或者是比较优雅地处理一个对象上的不可调用的方法而不是让编译器抛出异常。
这些情况就是使用钩子方法,但是它们的用法并不仅限于此。
不同的框架/库使用了不同的钩子方法来实现它们的功能。

from.instance_variables.each do |v|  
  to.instance_variable_set v, from.instance_variable_get(v)  
end 

复制代码 代码如下:

复制代码 代码如下:

复制代码 代码如下:

  eval会在当前环境执行字符串,除非环境已经提供绑定(binding)。(见第11条)

在这个类中定义 method_missing
是为了保证能够访问到那些未定义的方法,而不是由Ruby抛出异常。 在
method_missing 中它调用了 lookup 方法:

  Ruby有几种版本的执行方法(evaluation)。了解它们的区别和使用情景是很重要的。有eval、instance_eval、module_eval和class_eval几种。首先,class_eval是module_eval的别名。其次,eval和其他的有些不同。最重要的是eval只能够执行一个字符串,其它的可以执行block。这意味着eval是你做任何事的最后选择,它有它的用处,但绝大多数情况下应该用instance_eval和module_eval执行block。

复制代码 代码如下:

7. 使用nil类来引入空对象的重构 Use NilClass to implement the
Introduce Null Object refactoring

使用 prepend Person 会将 User
中的同名方法给覆盖掉,因此在终端输出的结果为 My name belongs to Person。
prepend 实际上是将方法添加到方法链的前端。在调用 User 类内定义的 name
方法时,会调用 super 从而调用 Person 模块的 name。

  在Fowler的重构中,“引入空对象”的重构是一个对象要么存在,要么为空时有一个预定义值。典型例子如下:

class User < Person
end

复制代码 代码如下:

  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?

body do 
  h1.header ‘Blog’ 
  div.content do 
    ‘Hellu’ 
  end 
end 

      if mod.const_defined?(“ClassMethods”)
        class_mod = mod.const_get(“ClassMethods”)
        extend class_mod

  这里令人感兴趣的是define_attr_method。在这个例子中我们需要获得Product类的单例类,但又不想修改ActiveRecord::Base。通过使用单例类我们达到了这个目的。我们为原来的方法取别名,再定义新的存取器(accessor)来返回值。如果ActiveRecord需要table
name就可以直接调用存取器。这种动态创建方法和存取器的技术在单例类是很常见的,特别是Rails。

复制代码 代码如下:

2. DSL的使用类方法来修改子类 Write DSL’s using class-methods that
rewrite subclasses

复制代码 代码如下:

  当你想创建一个DSL来定义类信息时,最常见的问题是怎样表示信息来让框架的其它部分使用。以定义一个ActiveRecord模型对象为例:

例如,如果我们想要编写一些代码在任何时候调用特定的方法都会返回一个静态字符串。
我们姑且将这个方法称作 name。你可能在其他地方也会想使用同一块代码。
这样最好是新建一个模块。让我们来创建一个:

  就可以这样使用hash:

复制代码 代码如下:

  这会创建一个新类,并赋给PersonVO,然后创建一个类的实例。从草稿创建新类并定义新方法也很简单:

method_missing
可能是Ruby中使用最广的钩子。在许多流行的Ruby框架/gem包/库中都有使用它。
当我们试图访问一个对象上不存在的方法时则会调用这个钩子方法。
让我们来看一个简单的例子:

def R(*urls); Class.new(R) { meta_def(:urls) { urls } };   
end 

Rake中的 method_missing

class Object    
  def add_tracing(*mths)      
    mths.each do |m|   
      singleton_class.send :alias_method, “traced_#{m}”, m   
      singleton_class.send :define_method, m do |*args|  
        $stderr.puts “before #{m}(#{args.inspect})” 
        ret = self.send(“traced_#{m}”, *args)  
        $stderr.puts “after #{m} – #{ret.inspect}” 
        ret  
      end 
    end    
  end 
  def remove_tracing(*mths)     
    mths.each do |m|  
      singleton_class.send :alias_method, m, “traced_#{m}” 
    end 
  end 
end 
“abc”.add_tracing :reverse 

Ruby给我们提供了一种方式使用 模块(modules) (在其他语言中被称作
混入类(mixins))来编写模块化的代码供其他的 模块/类 使用。 模块
的概念很简单,它就是一个可以在其他地方使用的独立代码块。

  1. 从block创建Proc并公开 Create Procs from blocks and send them around

class User
  extend Person
end

  把一个Proc实例化保存在变量中并公开的做法使得很多API容易使用。这是Markaby用来管理CSS
class定义的一种方法。很容易把block转换成Proc:
def create_proc(&p); p; end 
create_proc do 
  puts “hello” 
end       # => #<Proc …> 

rake hello[“Imran Latif”]
=> Hello Imran Latif

复制代码 代码如下:

prepended

  有时候一个方法的实现不是你要的,或者只做了一半。标准的面向对象方法是继承并重载,再调用父类方法。仅当你有对象实例化的控制权时才有用,经常不是这种情况,继承也就没有价值。为得到同样的功能,可以重命名(alias)旧方法,并添加一个新的方法定义来调用旧方法,并确保旧方法的前后条件得到保留。

让我们来看看 Rake 是如何使用 method_missing
为我们提供了传递给任务的参数的。

复制代码 代码如下:

p = Person.new

  绝大多数这种功能,特别是CSS
class名是通过method_missing设置了self的属性然后返回self。

puts User.new.name

sclass = (class << self; self; end) 

module Person
  def name
    puts “My name is Person”
  end
end

复制代码 代码如下:

复制代码 代码如下:

module Kernel  
  def singleton_class  
    class << self; self; end 
  end 
end 

class Person
  def name
    “My name is Person”
  end
end

复制代码 代码如下:

puts User.new.name # => My name is Person

  instance_eval会在接收者(reveiver)的上下文中执行字符串或block,没有指定的话self会作为接收者。

class User
  include Person
  def name
    “My name belongs to User”
  end
end

PersonVO = Struct.new(:name, :phone, :email)  
p1 = PersonVO.new(:name => “Ola Bini”) 

复制代码 代码如下:

  1. 用绑定(binding)来控制eval Use binding to control your
    evaluations**

class User
  prepend Person
  def name
    “My name belongs to User”
  end
end

  Rails使用了一个技巧来使controller中的实例变量也能用在view中,就是内省一个对象的实例变量。这会严重破坏封装,然而有时候确实非常顺手。可以很容易的通过instance_variables、instance_variable_get和instance_variable_set实现。要把所有实例变量从一个复制到另一个,可以这样:

复制代码 代码如下:

  如果要用proc来定义方法,应该用lambda来创建,就可以用return和break:
p = lambda { puts “hoho”; return 1 }  
define_method(:a, &p) 

该代码的运行结果是输出 User extended Person。

  Ruby允许你动态创建和修改class和module。你可以在没有冻结的class或module上做任何修改。特定情况下会很有用。Struct类可能是最好的例子:

Person prepended to User
My name belongs to Person

c = Class.new 
c.class_eval do 
  define_method :foo do 
    puts “Hello World” 
  end 
end 
c.new.foo    # => “Hello World” 

User included Person
My name is Person

复制代码 代码如下:

    self.devise_modules |= selected_modules
    options.each { |key, value| send(:”#{key}=”, value) }
  end
end

复制代码 代码如下:

puts User.new.name
=> My name belongs to User

  如果有block的话method_missing会调用block:
def method_missing(name, *args, &block)  
  block.call(*args) if block_given?  
end 
thismethoddoesntexist(“abc”,”cde”) do |*args|  
  p args  
end  # => [“abc”,”cde”] 

关于 extended 的介绍已经完了,让我们来看看 ActiveRecord 是如何使用它的。