我有一堆重复的方法,我相信我可以用某种方式使用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
方法将遵循一种模式。我希望学会使用元编程来避免重复。
我怎么能这样做?
答案 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
语句进行错误处理。
请注意,使用元编程可能会产生意外问题,包括:
eval
声明,则会出现安全问题。eval
语句时,请务必始终包含强大的错误处理。为了便于调试,您还可以使用元编程来动态生成首次启动程序时的代码。这样,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