@subject = subject
end
@subject.send sym, *args, &block
end
end
becomes
subject.public_methods(false).each do |meth|
(class << self; self; end).class_eval do
define_method meth do |*args|
subject.send meth, *args
end
end
end
end
end
Motivation
Debugging classes that use method_missing can often be painful. At best you often get a NoMethodError on an object that you didn't expect, and at worst you get stack level too deep (SystemStackError).
There are times that method_missing is required. If the usage of an object is unknown, but must support unexpected method calls you may not be able to avoid the use of method_missing.
However, often you know how an object will be used and using Dynamically Define Method you can achieve the same behavior without relying on method_missing.
Mechanics
- Dynamically define the necessary methods
- Remove method_missing
- Test
Delegation is a common task while developing software. Delegation can be handled explicitly by defining methods yourself or by utilizing something from the Ruby Standard Library such as Forwardable. Using these techniques gives you control over what methods you want to delegate to the subject object; however, sometimes you want to delegate all methods without specifying them. Ruby's Standard Library also provides this capability with the delegate library, but we'll assume we need to implement our own for this example.
The simple way to handle delegation (ignoring the fact that you would want to undefine all the standard methods a class gets by default) is to use method_missing to pass any method calls straight to the subject.
@subject = subject
end
@subject.send sym, *args, &block
end
end
This solution does work, but it can be problematic when mistakes are made. For example, calling a method that does not exist on the subject will result in the subject raising a NoMethodError. Since the method call is being called on the decorator, but the subject is raising the error it may be painful to track down where the problem resides.
The wrong object raising a NoMethodError is significantly better than the dreaded stack level too deep (SystemStackError). This can be caused by something as simple as forgetting to use the subject instance variable and trying to use a non-existent subject method or any misspelled method. When this happens the only feedback you have is that something went wrong, but Ruby isn't sure exactly what it was.
These problems can be avoided entirely by using the available data to dynamically define methods at run time. The following example defines an instance method on the decorator for each public method of the subject.
subject.public_methods(false).each do |meth|
(class << self; self; end).class_eval do
define_method meth do |*args|
subject.send meth, *args
end
end
end
end
end
Using this technique any invalid method calls will be correctly reported as NoMethodErrors on the decorator. Additionally, there's no method_missing definition, which should help avoid the stack level too deep problem entirely.
Example: Using user defined data to define methods
Often you can use the information from a class definition to define methods instead of relying on method_missing. For example, the following code relies on method_missing to determine if any of the attributes are nil.
attr_accessor :name, :age
empty?(sym.to_s.sub(/^empty_/,"").chomp("?"))
end
self.send(sym).nil?
end
end
The code works, but it suffers from the same debugging issues that the previous example does. Utilizing Dynamically Define Method the issue can be avoided by defining the attributes and creating the empty_attribute? methods at the same time.
attr_accessor *args
args.each do |attribute|
define_method "empty_ ?" do
self.send(attribute).nil?
end
end
end
attrs_with_empty_methods :name, :age
end
how about dynamically defining the methods when method missing is triggered?
ReplyDeleteex:
class Proxy
def method_missing(meth, *args, &block)
if @target.respond_to?(meth)
self.class.class_eval <<-end_eval
def #{meth}(*args, &block)
@target.__send__(:#{meth}, *args, &block)
end
end_eval
__send__(meth, *args, &block)
else
super # NoMethodError
end
end
end
this way, not all the instance methods are defined but only those that are actually used.
yuck, it stripped my spaces. but I think you get my idea.
ReplyDeleteJorrel,
ReplyDeleteI like the example, but since it still involves using method_missing I'd avoid it if possible.
But, it's a great alternative worth knowing about.
Cheers, Jay
Jay, if you are delegating all of the methods to subject, here is a simpler way:
ReplyDeleteclass Decorator
extend Forwardable
def initialize(subject)
@subject = subject
(class << self; self; end).class_eval do
def_delegators :'@subject', *subject.public_methods(false)
end
end
end
- Paul and Ali
Great example. I modified it to suit what i needed, and just now looking at the comments i realized i came up with something very similar to Jorrell... http://www.rubyrescue.com/?p=11
ReplyDeleteHow would you apply this when the initialize method is never called but allocate is used instead?
ReplyDeleteThanks, Daniel
You could redefine allocate, I imagine. Other than that, nothing comes to mind.
ReplyDeleteCheers, Jay
If you REALLY want to delegate all methods to the subject, why not just use the Delegator class from the standard library, which does it for you very easily?
ReplyDeletehttp://ruby-doc.org/stdlib/libdoc/delegate/rdoc/index.html
Thanks Jay,
ReplyDeletethis was super helpful. Because association_proxy calls send, it won't call method_missing on the target object. Thus any of your dynamic methods don't exist when you use them via the association.
This comment has been removed by the author.
ReplyDeleteDon't forget the block part:
ReplyDeleteclass Decorator
def initialize(subject)
subject.public_methods(false).each do |meth|
(class << self; self; end).class_eval do
define_method meth do |*args, &block|
subject.send meth, *args, &block
end
end
end
end
end
I ran into an issue with this methodology. If you Marshal the object when you reload the object the dynamically mapped methods no longer exist. You can "fix" this by creating a new instance of the class but that's not something downstream code should have to do.
ReplyDeleteIronically I'm trying to see if I can fix it by intercepting method missing.
Older article but still relevant. Thank you :)
ReplyDelete