如何使用Ruby的元编程来减少方法计数

我有一堆重复的方法,我相信我可以用某种方式使用Ruby的元编程。

我的class级看起来像这样:

class SomePatterns def address_key "..." end def user_key "..." end def location_key "..." end def address_count redis.scard("#{address_key}") end def location_count redis.scard("#{location_key}") end def user_count redis.scard("#{user_key}") end end 

我以为我只能有一种方法:

 def count(prefix) redis.scard("#{prefix}_key") # wrong, but something like this end 

以上是错误的,但我说*_count方法将遵循一种模式。 我希望学会使用元编程来避免重复。

我怎么能这样做?

我会将所有“函数前缀”放入一个数组中。 初始化后,您可以在这些前缀上使用:define_singleton_method ,在每个前缀上动态创建实例方法:

 class SomePatterns def initialize() prefixes = [:address, :location, :user] prefixes.each do |prefix| define_singleton_method("#{prefix}_count") { redis.scard("#{prefix}_key") } end end end 

编辑:

:define_singleton_method实际上可能:define_singleton_method矫枉过正。 它将适用于您想要的内容,但它将为该特定实例定义这些函数(因此称为单例)。 差异很微妙,但很重要。 相反,最好使用:class_eval和:define_method 。

 class SomePatterns # ... class_eval do [:address, :location, :user].each do |prefix| define_method("#{prefix}_count") { redis.scard("#{prefix}_key") } end end end 

你可以创建一个宏观风格的方法来整理。 例如:

创建一个新类Countable

 class Countable def self.countable(key, value) define_method("#{key}_count") do redis.scard(value) end # You might not need these methods anymore? If so, this can be removed define_method("#{key}_key") do value end end end 

inheritance自Countable ,然后只使用宏。 这只是一个示例 – 您还可以将其实现为ActiveSupport关注点,或扩展模块(如下面的注释中所示):

 class SomePatterns < Countable countable :address, '...' countable :user, '...' countable :location, '...' end 

我能想到的最简单的方法是使用所需的键值对循环Hash

 class SomePatterns PATTERNS = { address: "...", user: "...", location: "...", } PATTERNS.each do |key, val| define_method("#{key}_key") { val } define_method("#{key}_count") { redis.scard(val) } end end 

正如@ sagarpandya82建议的那样,你可以使用#method_missing 。 假设您希望缩短以下内容。

 class Redis def scard(str) str.upcase end end class Patterns def address_key "address_key" end def address_count redis.send(:scard, "address_count->#{address_key}") end def user_key "user_key" end def user_count redis.send(:scard, "user_count->#{user_key}") end def modify(str) yield str end private def redis Redis.new end end 

其行为如下:

 pat = Patterns.new #=> # pat.address_key #=> "address_key" pat.address_count #=> "ADDRESS_COUNT->ADDRESS_KEY" pat.user_key #=> "user_key" pat.user_count #=> "USER_COUNT->USER_KEY" pat.modify("what ho!") { |s| s.upcase } #=> "WHAT HO!" 

请注意,由于没有在类中定义对象redis ,我认为它是另一个类的实例,我将其命名为Redis

您可以通过更改类Patterns将方法数量减少到一个,如下所示。

 class Patterns def method_missing(m, *args, &blk) case m when :address_key, :user_key then m.to_s when :address_count, :user_count then redis.send(:scard, m.to_s) when :modify then send(m, *args, blk) else super end end private def redis Redis.new end end pat = Patterns.new #=> # pat.address_key #=> "address_key" pat.address_count #=> "ADDRESS_COUNT->ADDRESS_KEY" pat.user_key #=> "user_key" pat.user_count #=> "USER_COUNT=>USER_KEY" pat.modify("what ho!") { |s| s.upcase } #=> "WHAT HO!" pat.what_the_heck! #=> #NoMethodError:\ # undefined method `what_the_heck!' for # 

但是,这种方法存在一些缺点:

  • 使用method_missing的代码不像分别编写每种方法的传统方法那样容易理解。
  • 变量的数量以及块的存在与否都不会被强制执行
  • 调试可能很痛苦,堆栈溢出exception很常见。

由于其他人都非常慷慨地分享他们的答案,我想我也可以做出贡献。 我对这个问题的原始想法是利用模式匹配method_missing调用和一些通用方法。 (在这种情况下#key#count

然后我扩展了这个概念,允许自由forms初始化所需的前缀和键,这是最终结果:

 #Thank you Cary Swoveland for the suggestion of stubbing Redis for testing class Redis def sdcard(name) name.upcase end end class Patterns attr_accessor :attributes def initialize(attributes) @attributes = attributes end # generic method for retrieving a "key" def key(requested_key) @attributes[requested_key.to_sym] || @attributes[requested_key.to_s] end # generic method for counting based on a "key" def count(requested_key) redis.sdcard(key(requested_key)) end # dynamically handle method names that match the pattern # XXX_count or XXX_key where XXX exists as a key in attributes Hash def method_missing(method_name,*args,&block) super unless m = method_name.to_s.match(/\A(?\w+)_(?(count|key))\z/) and self.key(m[:key]) public_send(m[:meth],m[:key]) end def respond_to_missing?(methond_name,include_private= false) m = method_name.to_s.match(/\A(?\w+)_(?(count|key))\z/) and self.key(m[:key]) || super end private def redis Redis.new end end 

这允许以下实现,我认为它提供了一个非常好的公共接口来支持所请求的function

 p = Patterns.new(user:'some_user_key', address: 'a_key', multi_word: 'mw_key') p.key(:user) #=> 'some_user_key' p.user_key #=> 'some_user_key' p.user_count #=> 'SOME_USER_KEY' p.respond_to?(:address_count) #=> true p.multi_word_key #=> 'mw_key' p.attributes.merge!({dynamic: 'd_key'}) p.dynamic_count #=> 'D_KEY' p.unknown_key #=> NoMethodError: undefined method `unknown_key' 

显然,您可以预先定义属性,也不允许此对象的变异。

您可以通过中断method_missing的查找来调用所谓的魔术方法 。 这是一个基本的可validation示例,用于解释您如何处理解决方案:

 class SomePatterns def address_key "...ak" end def user_key "...uk" end def location_key "...lk" end def method_missing(method_name, *args) if method_name.match?(/\w+_count\z/) m = method_name[/[\w]+(?=_count)/] send("#{m}_key") #you probably need: redis.scard(send("#{m}_key")) else super end end end 

method_missing检查是否调用了以_count结尾的方法,如果是,则调用相应的_key方法。 如果相应的_key方法不存在,您将收到一条错误消息,告诉您这一点。

 obj = SomePatterns.new obj.address_count #=> "...ak" obj.user_count #=> "...uk" obj.location_count #=> "...lk" obj.name_count #test.rb:19:in `method_missing': undefined method `name_key' for # (NoMethodError) # from test.rb:17:in `method_missing' # from test.rb:29:in `
'

请注意,我们正在调用实际上未在任何地方定义的方法。 但是我们仍然会根据SomePatterns#method_missing定义的规则返回值或错误消息。

有关更多信息,请查看Russ Olsen的Eloquent Ruby ,特别是这个答案。 另请注意,值得了解BasicObject#method_missing工作原理,我不确定在专业代码中是否推荐上述技术(尽管我看到@CarySwoveland对此事有一些了解)。

你可以尝试类似的东西:

 def count(prefix) eval("redis.scard(#{prefix}_key)") end 

这会将前缀插入到将要运行的代码字符串中。 它没有您可能需要安全使用eval语句的error handling。

请注意,使用元编程可能会产生意外问题,包括:

  1. 如果用户输入进入您的eval语句,则会出现安全问题。
  2. 如果您提供的数据不起作用,则会出现错误。 使用eval语句时,请务必始终包含强大的error handling。

为了便于调试,您还可以使用元编程来动态生成首次启动程序时的代码。 这样, eval语句将不太可能在以后产生意外行为。 有关以这种方式执行此操作的更多详细信息,请参阅Kyle Boss的答案。

您可以使用class_eval创建一组方法

 class SomePatterns def address_key "..." end def user_key "..." end def location_key "..." end class_eval do ["address", "user", "location"].each do |attr| define_method "#{attr}_count" do redis.scard("#{send("#{attr}_key")}" end end end end