从源码上来看ruby标准库里的delegate.rb

译自这篇文章。这篇文章里涉及到了ruby元编程里的blank slate,以及method_missing的使用,一定程度上也是有些研究价值。简单翻译一下,以备后用

通常意义来说,OO编程就是在对象间传消息。当然,OO方式也鼓励我们使用相对准确的名词跟动词。可以把它想像成一个舞台剧,上面的参与者相互间在交流。有时,一个角色可能会通过一个第三者来与另一个角色进行交流,这种通过一个中间角色进行交流的方式就叫作代理(delegate)

先通过一个示例演示一下delegate是如何帮我们设计一个强壮且可扩展的接口。然后一起看一下delegate.rb的源码,来分析它是如何做到的。

给我提供一个电影的推荐吧

假设我们在做一个电影推荐的后端。简化的来看,我们的Moviescore的值是从iMDb跟烂番茄上得到的。假设它们的精度都是一样的。我们想要得到一个值average_score,它是两个值的平均值。代码如下:

class Movie
  attr_reader :imdb_score, :rotten_tomatoes_score

  def initialize(name, imdb_score, rotten_tomatoes_score)
    @name = name
    @imdb_score = imdb_score
    @rotten_tomatoes_score = rotten_tomatoes_score
  end

  def average_score
    (@imdb_score + @rotten_tomatoes_score) / 2
  end
end

接下去我们需要一个类来表示一个Movie的集合,就叫它为RecommendedMovies,我们可以这样来进行查询:

class RecommendedMovies
  def initialize(movies)
    @movies = movies
  end

  def best_by_imdb
    @movies.max_by(&:imdb_score)
  end

  def best_by_rotten_tomatoes
    @movies.max_by(&:rotten_tomatoes_score)
  end

  def best
    @movies.max_by(&:average_score)
  end
end

这很直观。添加一个测试代码:

north_by_northwest = Movie.new('North by Northwest', 85, 100)
inception = Movie.new('Inception', 88, 86)
the_dark_knight = Movie.new('The Dark Knight', 90, 94)

recommended_movies = RecommendedMovies.new([north_by_northwest, inception, the_dark_knight])

可以这样去查询:

recommended_movies.best=> #

有限的责任

上面这个类看上去挺好的,但有一个缺陷:我们是用一个array去进行初始化,但之后丢失了所有原来Array具备的行为:如果我们运行recommended_movies.count,会得到一个NoMethodError返回。我们有可能会想用到Array(以及Enumerable)里的一些功能,但现在都会报错。当然,我们可以通过实现method_missing来解决,但我们可以使用一种更优雅的方式来解决,Ruby标准包里的库——delegate.rb

这个库里给我们提供了两种比较具体的解决方法——两种都是通过继承 来实现的。DelegateClass值得单独再深入研究一下,更简单的一种方式是使用SimpleDelegator,它已经能满足我们上面的需求了。我们可以这样使用:

require 'delegate'

class RecommendedMovies < SimpleDelegator
  def best_by_imdb
    max_by(&:imdb_score)
  end

  def best_by_rotten_tomatoes
    max_by(&:rotten_tomatoes_score)
  end

  def best
    max_by(&:average_score)
  end
end

译注:跟最早的实现相比,这里有几点可以注意一下:

  1. require了 'delegate'包
  2. 继承自SimpleDelegator
  3. 方法体里没有def initialize方法
  4. 直接调用了max_by方法(这个是Enumerable里提供的方法),忽略了前面的receiver。

好了,现在所有都像之前一样可以使用,同时我们还有了array里的所有方法。基本上来说,我们是对一个array使用了一个装饰者模式(百度百科)。现在我们调用recommended_movies.count就会返回3了。

现象背后

源码地址。建议新开tab页打开,一边看学有源码一边看本文。可以使用l来跳转到指定行数(github自己的功能)。

在继承自SimpleDelegator之后,它的祖先链是这样的:

recommended_movies.class.ancestors
 => [RecommendedMovies, SimpleDelegator, Delegator,
#, BasicObject]

上面这祖先链跟我们以前认识的不太一样——[RecommendedMovies, Object, Kernel, BasicObject]。原因是SimpleDelegator继承自另一个类——Delegator(line 316)。而它是继承自BasicObject(line 39)。这就是为什么ObjectKernel不在这条祖先链里的原因。这个特殊的#是一个匿名module,在Delegate类里定义和引用(included)的(line 53);它就像是一个缩减版的KernelKernel被复制一份被放到一个临时变量里(line 40),之后,都在这个变量的类一级进行操作(line 41),并把一些方法undef_method掉。在这些变化做完后,这个kernel可以被Delegate引用了(include)。以上就解释了我们看到的这条祖先链。

透明的初始化(transparent initialization)

较早前我们有提到,这里忽略了RecommendedMovies里的initialize方法。Ruby在创建一个新的object时会自动调用initialize方法,因为我们在这里没有定义这个方法,它就会去祖先链里找。SimpleDelegator这里也没有实现这个方法,但Delegator有实现(line 71)。它期待一个单独的参数,obj,这个就是我们在创建RecommendedMovies实例时传入的参数,在我们的例子里就是一个MovieArray对象——也就是我们想要把消息代理过去的对象。

在内部,Delegator#initialize这个方法就是简单地调用了_setobj_方法,传递同样的这个obj参数。但Delegator没有实现_setobj_:如果直接调用它,会抛出一个异常(line 176)。这是因为Delegate扮演一个抽象类的角色。它的子孙类要去实现_setobj_方法,实际上SimpleDelegator也做了实现(line 340)。SimpleDelegator#__setobj__就是简单地把obj存在了一个名为delegate_sd_obj的实例变量里(sd意思为SimpleDelegator)。在我们的例子里,self仍然是recommended_movies

代理!

就像之前的示例,一旦我们的recommended_movies对象产生,我们就可以用它来装饰一个array。我们可以在它上面调用best方法,Ruby可以定位到这个对象的class,RecommendedMovies,并为我们执行它。但当我们调用count时,Ruby找不到对应的方法。之后去它的祖先链里去找,但也没有count方法。

这时就需要定义method_missing方法。如果Ruby在通常的方法查找过程中没有找到方法,它不会立即抛出NoMethodError方法;相反,它会继续查找,这次会去method_missing里找。如果任何一个祖先类里定义了这个方法,就会被调用到。如果没有的话,我们会收到NoMethodError错误。

在我们的上下文里,Delegator类定义了method_missing方法(line 78)。首先,它通过调用_getobj_方法,得到了我们想要代理过去的目标对象(line 80),是在(line 318)里实现的。实际上,这个方法是把我们存在@delegate_sd_obj里的对象拿了出来。之后用question方法试一下这个对象能否调用这个方法(line 83)。如果不行的话,Delegate#method_missing会检查是否Kernel可以调用这个方法,如果可以,就去调用(line 85),否则的话就会调用super(line 87),在这里,得到的结果就是NoMethodError

method_missing里还有些其他的代码,不过刚才说过的就是这里的核心部分。在《Ruby元编程》里有提到过“Blank Slate”,就是一个只有最小数量方法的类。Delegate类就是用到这个技术,它继承自BasicObject,消除了不必要意外,但同时也要注意到method_missing的实现,里面询问这个目标对象是否能repond一个特定的方法,这个目标对象一般会是继承自Object。这个内部原理有些复杂,但到最后,我们得到了一个比较简单且直观的接口(RecommendedMovies类)。没准在你的代码里也可以用到代理这个技术来进行一些重构。

你可能感兴趣的:(从源码上来看ruby标准库里的delegate.rb)