Monday 11 August 2014

Collection Proxy

Proxy

http://blog.rubybestpractices.com/posts/gregory/060-issue-26-structural-design-patterns.html




Proxy

Proxy is any object that acts as a drop-in replacement object that does a bit of work and then delegates to some other underlying object. This is another pattern that’s easy to recognize for Rails developers, because it is used extensively in ActiveRecord associations support. The following example shows a typical interaction between a base model and one of its associations.
  1. quiz = Quiz.first  
  2. quiz.questions.class #=> Array  
  3.   
  4. quiz.questions.count #=> 10  
  5.   
  6. quiz.questions.create(:question_text => "Who is the creator of Ruby?",  
  7.                       :answer        => "Matz")  
  8.   
  9. quiz.questions.count #=> 11  
  10.   
  11. quiz.questions[-1].answer #=> "Matz"   
While the questions association claims to be an array, it also provides some of the common helpers found in ActiveRecord::Base. If we stick to the core idea and ignore some of the Rails specific details, creating such an association proxy is fairly easy to do using Ruby’s delegate standard library. The code below more or less does the trick.
  1. require "delegate"  
  2.   
  3. class Quiz  
  4.   def questions  
  5.     @questions                  ||= HasManyAssociation.new([])  
  6.     @questions.associated_class ||= Question  
  7.   
  8.     @questions  
  9.   end  
  10. end  
  11.   
  12. class Question  
  13.   def initialize(params)  
  14.     @params = params  
  15.   end  
  16.   
  17.   attr_reader :params  
  18.   
  19.   def answer  
  20.     params[:answer]  
  21.   end  
  22. end  
  23.   
  24. class HasManyAssociation < DelegateClass(Array)  
  25.   attr_accessor :associated_class  
  26.   
  27.   def initialize(array)  
  28.     super(array)  
  29.   end  
  30.   
  31.   def create(params)  
  32.     self << associated_class.new(params)  
  33.   end  
  34. end  
  35.   
  36. quiz = Quiz.new  
  37.   
  38. # grab the proxy object  
  39. questions = quiz.questions  
  40.   
  41.   
  42. # use custom create() method on proxy object  
  43.   
  44. questions.create(:question_text => "Who is the creator of Ruby?",  
  45.                  :answer        => "Matz")  
  46. questions.create(:question_text => "Who is the creator of Perl?",  
  47.                  :answer        => "Larry Wall")  
  48.   
  49.   
  50. # use array like behavior on proxy object  
  51.   
  52. p questions[0].answer #=> "Matz"  
  53. p questions[1].answer #=> "Larry Wall"  
  54. p questions.map { |q| q.answer }.join(", "#=> "Matz, Larry Wall"  
Interestingly enough, while Ruby provides a standard library for building Proxy objects, most people tend to implement them in a different way, through the use of an explicitmethod_missing() hook on a blank slate object such as Ruby 1.9’s BasicObject. For example, we could have written our HasManyAssociation code in the manner shown below and things would still work as expected.
  1. class HasManyAssociation < BasicObject  
  2.   attr_accessor :associated_class  
  3.   
  4.   def initialize(array)  
  5.     @array = array  
  6.   end  
  7.   
  8.   def create(params)  
  9.     self << associated_class.new(params)  
  10.   end  
  11.   
  12.   def method_missing(*a, &b)  
  13.     @array.send(*a, &b)  
  14.   end  
  15. end  
Without looking at the source, I’m almost sure that Rails does something similar to this, because doing some_association.class returns Array rather than the name of the proxy object. This is the only noticeable difference on the surface between this approach and the DelegateClass approach.
Personally, I’ve written proxies in both ways, and I tend to prefer the DelegateClass()approach slightly, simply because it’s more explicit and doesn’t require me to explicitly define amethod_missing() hook. But on the other hand, we can see that rolling your own proxy implementation is trivial in Ruby, and some may prefer the self contained nature of doing the delegation work yourself. It’d be interesting to hear what readers have to say on this topic, so please feel free to post to the mailing list if you prefer one approach over the other.