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

时间:2017-04-13 19:32:46

标签: ruby metaprogramming

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

我的班级看起来像这样:

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方法将遵循一种模式。我希望学会使用元编程来避免重复。

我怎么能这样做?

8 个答案:

答案 0 :(得分:4)

我会把所有"函数前缀"成阵列。初始化后,您可以使用这些前缀上的: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实际上可能有点矫枉过正。它可以满足您的需求,但它将为该特定实例定义这些函数(因此称为单例)。差异很微妙,但很重要。相反,将: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

答案 1 :(得分:1)

正如@ 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                      #=> #<Patterns:0x007fe12b9968d0> 
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                      #=> #<Patterns:0x007fe12b9cc548>
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 #<Patterns:0x007fe12b9cc548>
然而,这种方法存在一些缺点:

  • 使用method_missing的代码并不像分别编写每种方法的传统方式那样容易理解。
  • 不强制执行变量的数量和存在或不存在
  • 调试可能很麻烦,堆栈溢出异常很常见。

答案 2 :(得分:1)

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

创建新课程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

继承Countable,然后只使用宏。这只是一个例子 - 您也可以例如将其作为ActiveSupport关注点实现,或扩展模块(如下面的评论中所示):

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

答案 3 :(得分:1)

我能想到的最简单的方法是使用所需的键值对遍历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

答案 4 :(得分:1)

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

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 #<SomePatterns:0x000000013b6ae0> (NoMethodError)
#        from test.rb:17:in `method_missing'
#        from test.rb:29:in `<main>'

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

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

答案 5 :(得分:1)

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

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

#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(?<key>\w+)_(?<meth>(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(?<key>\w+)_(?<meth>(count|key))\z/) and self.key(m[:key]) || super
  end
  private 
    def redis
      Redis.new
    end
end 

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

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'

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

答案 6 :(得分:0)

您可以尝试以下方式:

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

这会将前缀插入到将要运行的代码字符串中。 您可能需要安全地使用eval语句进行错误处理。

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

  1. 如果用户输入进入您的eval声明,则会出现安全问题。
  2. 如果您提供的数据不起作用,则会出错。使用eval语句时,请务必始终包含强大的错误处理。
  3. 为了便于调试,您还可以使用元编程来动态生成首次启动程序时的代码。这样,eval语句将不太可能在以后产生意外行为。有关这样做的详细信息,请参阅Kyle Boss的答案。

答案 7 :(得分:0)

您可以使用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