Ruby中的装饰器(从Python迁移)
今天我在从Python的角度学习Ruby。有一件事我完全搞不懂,那就是如何实现类似于装饰器的功能。为了简化问题,我想复制一个简单的Python装饰器:
#! /usr/bin/env python
import math
def document(f):
def wrap(x):
print "I am going to square", x
f(x)
return wrap
@document
def square(x):
print math.pow(x, 2)
square(5)
运行这个代码后,我得到了:
I am going to square 5
25.0
所以,我想创建一个函数 square(x)
,但是我希望在它执行之前能提醒我它要做什么。为了让事情更简单,我们去掉一些花哨的部分:
...
def square(x):
print math.pow(x, 2)
square = document(square)
...
那么,我该如何在Ruby中实现这个功能呢?这是我的第一次尝试:
#! /usr/bin/env ruby
def document(f)
def wrap(x)
puts "I am going to square", x
f(x)
end
return wrap
end
def square(x)
puts x**2
end
square = document(square)
square(5)
运行这个代码后生成了:
./ruby_decorate.rb:8:in `document': wrong number of arguments (0 for 1) (ArgumentError)
from ./ruby_decorate.rb:15:in `<main>'
我想这可能是因为括号不是必须的,所以它把我的 return wrap
当成了想要 return wrap()
。我不知道有什么方法可以在不调用函数的情况下引用它。
我尝试了各种其他方法,但都没有成功。
11 个回答
在Ruby中可以实现类似Python的装饰器。我不打算详细解释和举例,因为Yehuda Katz已经写了一篇很好的博客,讲解了Ruby中的装饰器DSL,所以我强烈推荐大家去看看:
更新:我在这个问题上得到了几个反对票,所以让我进一步解释一下。
alias_method (和 alias_method_chain)
并不完全等同于装饰器的概念。它只是重新定义方法实现的一种方式,而不使用继承(这样客户端代码不会察觉到差别,仍然使用相同的方法调用)。这可能会有用,但也可能容易出错。任何使用过Ruby的Gettext库的人可能会注意到,它的ActiveRecord集成在每次Rails重大升级时都会出现问题,因为别名版本一直遵循旧方法的语义。
一般来说,装饰器的目的是不改变任何给定方法的内部实现,同时仍然能够从修改过的版本中调用原始方法,而是增强函数的行为。类似于alias_method_chain
的“入口/出口”用例,只是一个简单的演示。另一种更有用的装饰器可能是@login_required
,它检查授权,只有在授权成功时才运行函数,或者@trace(arg1, arg2, arg3)
,它可以执行一系列跟踪程序(并可以用不同的参数调用以装饰不同的方法)。
好吧,现在我来试着回答这个问题。我主要是想帮助那些想要重新整理思路的Python爱好者。下面是一些详细注释的代码,它大致上实现了我最初想做的事情:
装饰实例方法
#! /usr/bin/env ruby
# First, understand that decoration is not 'built in'. You have to make
# your class aware of the concept of decoration. Let's make a module for this.
module Documenter
def document(func_name) # This is the function that will DO the decoration: given a function, it'll extend it to have 'documentation' functionality.
new_name_for_old_function = "#{func_name}_old".to_sym # We extend the old function by 'replacing' it - but to do that, we need to preserve the old one so we can still call it from the snazzy new function.
alias_method(new_name_for_old_function, func_name) # This function, alias_method(), does what it says on the tin - allows us to call either function name to do the same thing. So now we have TWO references to the OLD crappy function. Note that alias_method is NOT a built-in function, but is a method of Class - that's one reason we're doing this from a module.
define_method(func_name) do |*args| # Here we're writing a new method with the name func_name. Yes, that means we're REPLACING the old method.
puts "about to call #{func_name}(#{args.join(', ')})" # ... do whatever extended functionality you want here ...
send(new_name_for_old_function, *args) # This is the same as `self.send`. `self` here is an instance of your extended class. As we had TWO references to the original method, we still have one left over, so we can call it here.
end
end
end
class Squarer # Drop any idea of doing things outside of classes. Your method to decorate has to be in a class/instance rather than floating globally, because the afore-used functions alias_method and define_method are not global.
extend Documenter # We have to give our class the ability to document its functions. Note we EXTEND, not INCLUDE - this gives Squarer, which is an INSTANCE of Class, the class method document() - we would use `include` if we wanted to give INSTANCES of Squarer the method `document`. <http://blog.jayfields.com/2006/05/ruby-extend-and-include.html>
def square(x) # Define our crappy undocumented function.
puts x**2
end
document(:square) # this is the same as `self.document`. `self` here is the CLASS. Because we EXTENDED it, we have access to `document` from the class rather than an instance. `square()` is now jazzed up for every instance of Squarer.
def cube(x) # Yes, the Squarer class has got a bit to big for its boots
puts x**3
end
document(:cube)
end
# Now you can play with squarers all day long, blissfully unaware of its ability to `document` itself.
squarer = Squarer.new
squarer.square(5)
squarer.cube(5)
还是觉得困惑吗?我一点也不惊讶;这让我花了将近一天的时间。还有一些其他你需要知道的事情:
- 首先,非常重要的一点是要阅读这个链接:http://www.softiesonrails.com/2007/8/15/ruby-101-methods-and-messages。在Ruby中,当你调用'foo'时,实际上是在给它的拥有者发送一条消息:“请调用你的方法'foo'”。你无法像在Python中那样直接操作函数;它们在Ruby中是滑溜溜的,难以捉摸。你只能把它们看作是洞穴墙上的影子;你只能通过字符串或符号来引用它们,这些字符串或符号恰好是它们的名字。试着把你在Ruby中每次调用的方法'object.foo(args)'想象成在Python中的这个:'object.getattribute('foo')(args)'。
- 停止在模块/类外部定义任何函数/方法。
- 从一开始就接受这个学习过程会让你感到脑袋发懵,慢慢来。如果Ruby让你感到困惑,可以去打一下墙,泡杯咖啡,或者睡一觉。
装饰类方法
上面的代码是用来装饰实例方法的。如果你想直接装饰类上的方法呢?如果你阅读http://www.rubyfleebie.com/understanding-class-methods-in-ruby,你会发现有三种创建类方法的方法——但这里只有一种适合我们。
那就是匿名的class << self
技巧。让我们来做上面的事情,这样我们就可以在不实例化的情况下调用square()和cube():
class Squarer
class << self # class methods go in here
extend Documenter
def square(x)
puts x**2
end
document(:square)
def cube(x)
puts x**3
end
document(:cube)
end
end
Squarer.square(5)
Squarer.cube(5)
玩得开心!
这里有另一种方法,可以解决别名方法之间名字冲突的问题(注意,我之前提到的使用模块装饰的方法也是一个不错的选择,因为它同样可以避免冲突):
module Documenter
def document(func_name)
old_method = instance_method(func_name)
define_method(func_name) do |*args|
puts "about to call #{func_name}(#{args.join(', ')})"
old_method.bind(self).call(*args)
end
end
end
上面的代码之所以能工作,是因为 old_method
这个局部变量在新的 'hello' 方法中保持活跃,这是因为 define_method
的代码块是一个闭包。