class Ticket
attr_accessor :status
[:planned, :pre_sale, :on_sale].each do |method_name|
define_method :"#{method_name}?" do
status == method_name
end
end
end
The above code works, but I find it painful to maintain. When I begin to read
[:planned, :pre_sale, :on_sale].each do |method_name| I'm not expecting to find methods being defined. Often, I define class methods that give me a more descriptive way to generate the similar methods. In this case I would consider creating a create_status_boolean_methods method. If I went down that path the code could look like the example found below.class Ticket
attr_accessor :status
def self.create_status_boolean_methods(*methods)
methods.each do |method_name|
define_method :"#{method_name}?" do
status == method_name
end
end
end
create_status_boolean_methods :planned, :pre_sale, :on_sale
end
This solution is decent, but there are a few things I don't like about it. Firstly, it's usually not important to understand how the
create_status_boolean_methods method does it's job. Second, the definition of create_status_boolean_methods is often not near the actual usage of create_status_boolean_methods; therefore, on the rare occasion that you do need to understand how the methods are being defined you'll have to do a little searching within the class. Third, and perhaps the most obvious, it would take less code to define each method on it's own.The above option is a good solution in some cases; however, Neal and I were looking for something simpler that didn't suffer from the above list of limitations. We came up with the following generalized solution to defining similar methods.
class Class
def def_each(*method_names, &block)
method_names.each do |method_name|
define_method method_name do
instance_exec method_name, &block
end
end
end
endGiven the
def_each method defined on Class the Ticket class becomes easier to read in my opinion. The reason it's easier (in my opinion) is because when I'm scanning the file and I see def_each :planned?, :pre_sale?, :on_sale? do |method_name| I know that similar methods are being defined. Below is the full code now required to define the Ticket class.class Ticket
attr_accessor :status
def_each :planned?, :pre_sale?, :on_sale? do |method_name|
:"#{status}?" == method_name
end
endYou may have noticed the
instance_exec method call in the implementation of the def_each method. I've previously written about instance_exec, but I'll include the implementation here for completeness.class Object
module InstanceExecHelper; end
include InstanceExecHelper
def instance_exec(*args, &block)
begin
old_critical, Thread.critical = Thread.critical, true
n = 0
n += 1 while respond_to?(mname="__instance_exec#{n}")
InstanceExecHelper.module_eval{define_method(mname, &block) }
ensure
Thread.critical = old_critical
end
begin
ret = send(mname, *args)
ensure
InstanceExecHelper.module_eval{remove_method(mname) } rescue nil
end
ret
end
end
3 comments:
This article is a keeper. Great summary.
Reply DeleteFor the particular example, I agree that the last is nicest.
Jay, some of the underscores seem to be escaping from your code when being formatted for colour in the article.
Reply DeleteYou'll have to tighten up security :)
My favorite part of this idea is that it would make it easy to solve a really tricky problem: having such methods show up in rdocs. It wouldn't be too hard to modify rdoc to recognize the def_each method and, in many of the cases where it would be used, have the methods show up properly in rdocs. For example, Rails could have proper documentation of the get, post, put, delete, etc. methods in ActionController::TestProcess.
Reply DeleteNote: Only a member of this blog may post a comment.
[フレーム]