August 07, 2009
Older: Getting Started With MongoMapper and Rails
Newer: MongoMapper Indy.rb Presentation
Patterns Are Not Scary: Method Missing Proxy
Method missing proxy? Ooooh! Sounds scary, right? I got news for you Walter Cronkite, it’s not. Lets start with the definition of proxy, according to Wikipedia.
Definition
A proxy, in its most general form, is a class functioning as an interface to something else.
An interface to something else. That sounds easy enough. You might be thinking that you have never used a proxy, but if you are reading this blog, you are wrong. Chances are you have used Rails, and if you have used Rails, chances are you have used has_many or some other ActiveRecord association, all of which are implemented using proxies under the hood.
Creating Your Own
Now that we have definition out of the way and have confirmed your use of proxies, let’s make one! Yay! The people rejoice! The basic idea of a proxy is a class that is an interface to something else. Lets call something else subject from now on. In order to get started, we’ll make a new proxy that has a subject.
class Proxy
def initialize(subject)
@subject = subject
end
end
proxied_array = Proxy.new([1,2,3])
puts proxied_array.size
# NoMethodError: undefined method ‘size’
FAIL! Our proxy has a subject (the array), but does not proxy anything yet. In order to proxy up the girl, lets throw in some method missing magic.
class Proxy
def initialize(subject)
@subject = subject
end
private
def method_missing(method, *args, &block)
@subject.send(method, *args, &block)
end
end
proxied_array = Proxy.new([1,2,3])
puts proxied_array.size # 3
Method missing takes 3 arguments: the method called, the arguments passed to the method and a block if one is given. With just that tiny method missing addition, we can now do fun things like this:
proxied_array = Proxy.new([1,2,3])
puts proxied_array.size # 3
puts proxied_array[0] # 1
puts proxied_array[1] # 2
puts proxied_array[2] # 3
puts proxied_array.select { |a| a > 1 }.inspect # [2, 3]
proxied_array << 4
puts proxied_array.size # 4
puts proxied_array[3] # 4
Just like that our proxied array behaves just like the original array. Well, almost like the original array.
puts proxied_array.class # Proxy
BlankSlate and BasicObject
Hmm, that is not quite what you would expect. We told the proxy to send everything to the subject, so it should output Array, not Proxy as the class, right? The problem is that any new class automatically has some methods included with it. In order for our Proxy class to be a true proxy, we need to remove those methods as well. In the Ruby 1.8 series, this is often done by defining a BlankSlate object which removes those methods and then have our Proxy inherit from BlankSlate.
class BlankSlate #:nodoc:
instance_methods.each { |m| undef_method m unless m =~ /^__|instance_eval|object_id/ }
end
class Proxy < BlankSlate
def initialize(subject)
@subject = subject
end
private
def method_missing(method, *args, &block)
@subject.send(method, *args, &block)
end
end
proxied_array = Proxy.new([1,2,3])
puts proxied_array.class # Array
Yay! Now we in fact get Array as one would expect. The great news is that Ruby 1.9 comes with a class like this already named BasicObject. The easy way to make this work with Ruby 1.8 and Ruby 1.9 is to just define BasicObject if it does not exist and then inherit from BasicObject, instead of dealing with BlankSlate.
class BasicObject #:nodoc:
instance_methods.each { |m| undef_method m unless m =~ /^__|instance_eval/ }
end unless defined?(BasicObject)
class Proxy < BasicObject
def initialize(subject)
@subject = subject
end
private
def method_missing(method, *args, &block)
@subject.send(method, *args, &block)
end
end
proxied_array = Proxy.new([1,2,3])
puts proxied_array.class # Array
Just like that our proxy is a full fledged proxy and it works with Ruby 1.8 and 1.9.
Example: MongoMapper Pagination
So other than ActiveRecord where else can you check out some proxies in the wild? In MongoMapper, pagination uses a method missing proxy. When someone uses paginate, instead of find, I wanted the result that was returned to also function much like WillPaginate::Collection does, but I didn’t want to inherit from Array.
You can view the pagination proxy on github. The paginate method that uses it looks like this:
def paginate(options)
per_page = options.delete(:per_page)
page = options.delete(:page)
total_entries = count(options[:conditions] || {})
collection = Pagination::PaginationProxy.new(total_entries, page, per_page)
options[:limit] = collection.limit
options[:offset] = collection.offset
collection.subject = find_every(options)
collection
end
Just like that, paginate returns results just like find, but also includes methods for total_pages, previous and next pages, total_entries and the like.
Example: HTTParty Response
In HTTParty, at first I just returned a ruby hash that was the parsed xml or json. Then, people started begging for response codes and headers, so I went with a Response proxy that looks like this:
module HTTParty
class Response < BlankSlate #:nodoc:
attr_accessor :body, :code, :message, :headers
attr_reader :delegate
def initialize(delegate, body, code, message, headers={})
@delegate = delegate
@body = body
@code = code.to_i
@message = message
@headers = headers
end
def method_missing(name, *args, &block)
@delegate.send(name, *args, &block)
end
end
end
Now I just pass the parsed response, the codes, headers, and such to Response.new and the people who want that information get it and those who don’t have no API change to wrestle with.
Conclusion
Hope this little primer on the Proxy pattern, specifically using Ruby’s method missing, is helpful. I also hope that because of this you check out some of the other great patterns that are out there. I know I avoided them for far too long. When applied correctly, they really lead to elegant solutions.
9 Comments
Aug 06, 2009
Good article John. One pattern I’ve been a proponent of recently is the decorator used for presenters; method missing is an integral part of this, and BasicObject looks to be a perfect fit in my implementation. Thanks!
Aug 07, 2009
Why not use the Delegate class?
http://www.ruby-doc.org/stdlib/libdoc/delegate/rdoc/index.html
When asked what the proxy’s class is, it should say ProxyArray not Array. If the proxy doesn’t give some clue as to what it truly is, you will add confusion later down the line when you have received this proxy in another method and ask what it is, then try to troubleshoot from there.
Aug 07, 2009
@Josh – No problem.
@Jeremy – I forgot about SimpleDelegator. Looks like other than the output of class, it is pretty similar to method missing proxy. Good reminder. Not sure if there are any other differences, but I’ll start playing with it.
I haven’t run into issues with needing the proxy class to identify itself as a proxy instead of the proxy subject, but if I do I’ll keep that in mind.
Aug 10, 2009
Great post. It’s funny, I just made a post over this same topic a few days ago:
http://www.binarylogic.com/2009/08/07/how-to-create-a-proxy-class-in-ruby/
But yours seems to be much more thorough.
Aug 11, 2009
I’m in a bit of a “I don’t get it” state here. I just don’t get it. I don’t see that the uses given here match any of the purposes for the GOF Proxy pattern, for example. I get (and appreciate and like) the “how”, I’m just struggling with the “why”.
Looking at the examples, we want to build something that says it’s an Array and behaves exactly like an Array except when it doesn’t. But it only doesn’t behave exactly like an Array (which it insists it is) in certain circumstances. Sort of locally monkey-patched, if you will.
I didn’t want to inherit from Array.
Well why the heck not? If you want something to behave exactly the same as something else but with some specific extensions/decorations, isn’t that exactly what inheritance is for? We still pass Liskov Substitution, don’t we? (Because the subclass can be used anywhere its super class is).
Help me be smarter…
Aug 11, 2009
Isn’t this the strategy pattern? Or am I missing something?
Aug 15, 2009
@Mike – So Jeremy is probably right in that it should identify itself as a ProxyArray instead of an Array in this case. My example may not be top notch. I’ve heard a lot about it being bad to inherit from concrete classes such as String, Array and such so that is why I didn’t want to.
@burtella Very similar. I’m going to do a post on the strategy pattern as well before too long. Hopefully that will clear up the difference.
Aug 16, 2009
Can not make it works.
Got error message:
superclass mismatch for class Proxy
ruby -v
ruby 1.8.7 (2009-04-08 patchlevel 160) [i386-freebsd7]
Aug 16, 2009
Oops. Sorry. Not that version of ruby.
Last example does not work with
ruby19 -v
ruby 1.9.1p129 (2009-05-12 revision 23412) [i386-freebsd7]
Sorry, comments are closed for this article to ease the burden of pruning spam.