从源码上来看ruby标准库里的delegate.rb
译自这篇文章。这篇文章里涉及到了ruby元编程里的blank slate,以及method_missing的使用,一定程度上也是有些研究价值。简单翻译一下,以备后用
通常意义来说,OO编程就是在对象间传消息。当然,OO方式也鼓励我们使用相对准确的名词跟动词。可以把它想像成一个舞台剧,上面的参与者相互间在交流。有时,一个角色可能会通过一个第三者来与另一个角色进行交流,这种通过一个中间角色进行交流的方式就叫作代理(delegate)。
先通过一个示例演示一下delegate是如何帮我们设计一个强壮且可扩展的接口。然后一起看一下delegate.rb
的源码,来分析它是如何做到的。
给我提供一个电影的推荐吧
假设我们在做一个电影推荐的后端。简化的来看,我们的Movie
的score
的值是从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=> #<Movie:0x007fbcf7048948 [@name](http://twitter.com/name)="North by Northwest", [@imdb_score](http://twitter.com/imdb_score)=85, [@rotten_tomatoes_scor](http://twitter.com/rotten_tomatoes_scor)e=100>
有限的责任
上面这个类看上去挺好的,但有一个缺陷:我们是用一个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
译注:跟最早的实现相比,这里有几点可以注意一下:
- require了 'delegate'包
- 继承自
SimpleDelegator
- 方法体里没有
def initialize
方法 - 直接调用了
max_by
方法(这个是Enumerable
里提供的方法),忽略了前面的receiver。
好了,现在所有都像之前一样可以使用,同时我们还有了array里的所有方法。基本上来说,我们是对一个array使用了一个装饰者模式(百度百科)。现在我们调用recommended_movies.count
就会返回3了。
现象背后
源码地址。建议新开tab页打开,一边看学有源码一边看本文。可以使用l
来跳转到指定行数(github自己的功能)。
在继承自SimpleDelegator
之后,它的祖先链是这样的:
recommended_movies.class.ancestors
=> [RecommendedMovies, SimpleDelegator, Delegator,
#<Module:0x007fed5005fc90>, BasicObject]
上面这祖先链跟我们以前认识的不太一样——[RecommendedMovies, Object, Kernel, BasicObject]
。原因是SimpleDelegator
继承自另一个类——Delegator
(line 316)。而它是继承自BasicObject
(line 39)。这就是为什么Object
跟Kernel
不在这条祖先链里的原因。这个特殊的#<Module:0x007fed5005fc90>
是一个匿名module,在Delegate
类里定义和引用(included)的(line 53);它就像是一个缩减版的Kernel
:Kernel
被复制一份被放到一个临时变量里(line 40),之后,都在这个变量的类一级进行操作(line 41),并把一些方法undef_method
掉。在这些变化做完后,这个kernel可以被Delegate
引用了(include)。以上就解释了我们看到的这条祖先链。
透明的初始化(transparent initialization)
较早前我们有提到,这里忽略了RecommendedMovies
里的initialize
方法。Ruby在创建一个新的object时会自动调用initialize
方法,因为我们在这里没有定义这个方法,它就会去祖先链里找。SimpleDelegator
这里也没有实现这个方法,但Delegator
有实现(line 71)。它期待一个单独的参数,obj
,这个就是我们在创建RecommendedMovies
实例时传入的参数,在我们的例子里就是一个Movie
的Array
对象——也就是我们想要把消息代理过去的对象。
在内部,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
类)。没准在你的代码里也可以用到代理这个技术来进行一些重构。