Reputation: 266988
I have a bunch of methods that are repeating, and I am sure I can use Ruby's metaprogramming somehow.
My class looks like this:
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
I was thinking I could have only one method like:
def count(prefix)
redis.scard("#{prefix}_key") # wrong, but something like this
end
The above is wrong, but I'm saying that the *_count
methods will follow a pattern. I'm hoping to learn to use metaprogramming to avoid the duplication.
How could I do this?
Upvotes: 4
Views: 306
Reputation: 9497
You can call so-called magic methods by interrupting the look-up of method_missing
. Here is a basic verifiable example to explain how you may approach your solution:
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
checks to see if a method ending in _count
was called, if so the corresponding _key
method is called. If the corresponding _key
method does not exist, you'll receive an error message telling you this.
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>'
Note we're calling methods that aren't actually defined anywhere. But we still return a value or error message according to the rules defined in SomePatterns#method_missing
.
For further info check out Eloquent Ruby by Russ Olsen, from which this answer references in particular. Note also it's worth understanding how BasicObject#method_missing
works in general and I am not at all sure if the above technique is recommended in professional code (though I see @CarySwoveland has some insight on the matter).
Upvotes: 1
Reputation: 29318
Since everyone else has been so gracious in sharing their answers I thought I might contribute as well. My original thought to this question was to utilize a pattern matching method_missing
call and a few generic methods. (#key
and #count
in this case)
I then expanded this concept to allow for a free form initialization of the desired prefixes and keys and this is the end result:
#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
This allows for the following implementation which I think offers a very nice public interface to support the requested functionality
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'
Obviously you could pre-define what attributes is and not allow for mutation of this object as well.
Upvotes: 1
Reputation: 110675
As @sagarpandya82 suggests, you could use #method_missing. Suppose you wish to shorten the following.
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
which behaves like so:
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!"
Note that since the object redis
was not defined in the class I assumed it to be an instance of another class, which I named Redis
.
You could reduce the number of methods to one, by changing class Patterns
as follows.
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>
There are, however, some disadvantages with this approach:
method_missing
is not as easily understood as the conventional way of writing each method separately.Upvotes: 1
Reputation: 708
The simplest way I could think of would be to loop through a Hash
with the needed key-value pairs.
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
Upvotes: 1
Reputation: 5690
You could create a macro-style method to tidy things up. For example:
Create a new class 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
Inherit from Countable
and then just use the macro. This is just an example - you could also e.g. implement it as an ActiveSupport Concern, or extend a module (as suggested in the comments below):
class SomePatterns < Countable
countable :address, '...'
countable :user, '...'
countable :location, '...'
end
Upvotes: 1
Reputation: 226
I would put all of the "function prefixes" into an array. Upon initialization you can use the :define_singleton_method
on these prefixes to dynamically create a instance method on every one of them:
class SomePatterns
def initialize()
prefixes = [:address, :location, :user]
prefixes.each do |prefix|
define_singleton_method("#{prefix}_count") { redis.scard("#{prefix}_key") }
end
end
end
EDIT:
:define_singleton_method
might actually be overkill. It will work for what you want, but it will define these functions for that specific instance (hence why its called singleton). The difference is subtle, but important. Instead, it would probably be better to use :class_eval in conjunction with :define_method.
class SomePatterns
# ...
class_eval do
[:address, :location, :user].each do |prefix|
define_method("#{prefix}_count") { redis.scard("#{prefix}_key") }
end
end
end
Upvotes: 4
Reputation: 494
You can use class_eval to create a group of methods
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
Upvotes: 0
Reputation: 1197
You could try something like:
def count(prefix)
eval("redis.scard(#{prefix}_key)")
end
This interpolates the prefix into a string of the code that will be run. It does not have the error handling you likely need to safely use an eval
statement.
Note that using metaprogramming can produce unexpected issues including:
eval
statement.eval
statements.For easier debugging, you could also use metaprogramming to dynamically generate the code you have above when you first start the program. That way the eval
statement will be less likely to produce unexpected behavior later on. See Kyle Boss's answer for more details on doing it this way.
Upvotes: 0