include 陷阱

include 定义:

include(module, ...) → self click to toggle source

Invokes Module.append_features on each parameter in reverse order.

append_features(mod) → mod click to toggle source

When this module is included in another, Ruby calls append_features in this module, passing it the receiving module in mod. Ruby’s default implementation is to add the constants, methods, and module variables of this module to mod if this module has not already been added to mod or one of its ancestors. See also Module#include.

include order

举个例子:

module A
end

class Foo
  include A
end

p Foo.ancestors

输出:

[Foo, A, Object, Kernel, BasicObject]

对于Foo,它includeA,那么Aancestor chain 上就被 appendFoo 的后面。

对于多个include的情况:

module A
end
module B
end

class Foo
  include A
  include B
end

class Bar
  include A, B
end

p Foo.ancestors
p Bar.ancestors

输出:

[Foo, B, A, Object, Kernel, BasicObject]
[Bar, A, B, Object, Kernel, BasicObject]

这里使用了两种include多个module的语法:

include scope

举个例子:

module B
end

module A
  include B
end

module C
end

module B
  include C
end


p A.ancestors
p B.ancestors

输出:

[A, B]
[B, C]

这里之所以 A.ancestors 不等于 [A, B, C] ,是因为,上面的语句等价于:

module A
end
module B
end
module C
end

A.include B
B.include C

在执行 A.include B之后的作用是将Bappend到A这个Class对象的ancestor chain中。接着,B.include C改变了Bancestor chain,但是B的 chain 不会影响 A的 chain。

这揭示了一点:如果某个类 C 已经include了某个 module M,之后如果 open M 在其中 include 了另一个 module M2,那么这不会作用于 C。只能 open M 在里边定义新的 method/const, 或者 open C,在里面 include M2.

method_missing

method_missing 可以用来实现 Ghost Method,即如果某个method没有找到,ruby会调用 method_missing 方法,在这个方法中开发者可以定义各种行为,使用户感觉他调用的方法实际是存在的。

但是,这里需要注意: 所有在 method_missing 中实现的 Ghost Method,实际是不存在的。因此,当用户调用 respond_to? 检查是否存在该method的时候,返回false。这导致了不一致性。解决的方法是,定义respond_to_missing,在其中判断传入的method是否是已经实现的 Ghost Method,如果是的话返回 true,否则调用 super。也就是说,method_missingrespond_to_missing 总是配对定义的.

值得一提的是,使用 Ghost Method 的时候,容易遇到一个情况是,你认为某个方法不存在,也即应该由 method_missing 来处理,但是这个方法却被某个父类定义了(例如 Object#display),这种bug是很难在一开始就预见的。解决的方案是,显示地指定你的父类为 BasicObject,这个类只定义了以下几个 instance_method:

pry(main)> BasicObject.instance_methods
=> [:equal?, :!, :__binding__, :==, :instance_exec, :!=, :instance_eval, :__id__, :__send__]

这时候,可以称 BasicObject 是一个 Blank Slate。另外,这时候由于父类中没有定义 respond_to? 方法,因此,我们也不需要定义respond_to_missing.

最后,慎用 Ghost Method,只有当所有可以调用的method的集合是未知的情况可以考虑。否则,考虑使用 Dynamic Method + Dynamic Dispatch的方式,显示定义method。

erb debug

Ruby的erb模板文件可以使用 Rubymine 进行调试,但是有一个前提条件,你需要提供erb模板文件的路径,例如:

erb = ERB.new(File.read(filename))
erb.filename = filename

接下来,你就可以在erb中断点调试了 :)

封装已有代码

这里主要讨论如何在不修改已有代码(例如第三方的库中的代码),扩展某些类的某些方法。扩展后的代码有时候仅仅服务于新写的代码,有时候也可能希望这个扩展是全局的,即可以影响到第三方库中的其他调用方。

下面通过一个简单的例子介绍三种扩展方法:

考虑有一个第三方类定义如下:

# FILE: foo.rb
class Foo
  def greeting
    'Hello'
  end
end

Around Aliases

使用 alias/alias_method将原方法拷贝到一个alias符号中,然后重新定义该方法(可以在定义中使用alias的原方法):

# FILE: foo_wrapper.rb
require 'foo.rb'

class Foo
# or: 
#Foo.class_eval do
  alias :old_greeting :greeting
  def greeting
    "#{old_greeting} magodo"
  end
end

任何require了 foo_wrapper.rb 的文件都会有这个新的改动.

Refinement

# FILE: foo_wrapper.rb
require 'foo.rb'

module FooRefinement
  refine Foo do
    def greeting
      "#{old_greeting} magodo"
    end
  end
end

这种方法适合当你需要严格控制在某几个特定的地方才会使用这个封装之后的方法,其他地方依然使用原来的方法的情况。当需要使用的时候,在开始加上: using FooRefinement.

Prepend

# FILE: foo_wrapper.rb
require 'foo.rb'

module FooWrapper
  def greeting
    "#{old_greeting} magodo"
  end
end

class Foo
# or: 
#Foo.class_eval do
  prepend FooWrapper
end

这种方法也是改动在require了文件之后就会生效.