Skip to content

Instantly share code, notes, and snippets.

@eric1234
Last active March 22, 2024 12:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eric1234/10165468 to your computer and use it in GitHub Desktop.
Save eric1234/10165468 to your computer and use it in GitHub Desktop.
Generic Ruby Proxy Module

Purpose

A generic proxy module.

Since it's a module it can be mixed into any class. This differs from Ruby's Delegator class which require the inheritance chain to include the Delegator module. Since it implements it's magic via method_missing, it will only proxy the method if the method doesn't exist on the base class or any of it's parent classes.

Example

class Crazy
  def foo
    'bar'
  end

  include Proxy
  def __proxy_object
    "bazboo"
  end

end

obj = Crazy.new
obj.foo   # => 'bar'
obj.size  # => 6

ActiveRecord Considerations

If using this to load related data for ActiveRecord you will want to take care to avoid N+1 loading. For example:

class User < ActiveRecord::Base
  include Proxy
  has_one :profile, dependent: :destroy
  alias_method :__proxy_object, :profile
end
class Profile < ActiveRecord::Base
  belongs_to :user
end

The above seems like a good way to merge to objects into a logically single object. So if Profile contained a field "address" you can do:

User.first.address

It will automatically see that address is not a method on User so it will proxy the call off to the Profile record. But if we try to load many objects:

User.all.collect &:address

We easily trigger a N+1 loading of Profile. Even pre-loading doesn't solve the problem:

User.includes(:profile).collect &:address

The reason includes doesn't solve the problem is because ActiveRecord will make undefined method calls on your object before the related object is loaded. The only solution I have found is to test for preloaded data:

class User < ActiveRecord::Base
  include Proxy
  has_one :profile, dependent: :destroy
  def __proxy_object
    profile if association(:profile).loaded?
  end
end
class Profile < ActiveRecord::Base
  belongs_to :user
end

Now our automatic proxying doesn't work unless we preload. But if we preload it works and avoids N+1. So:

User.first.address                    # Throws NoMethodError
User.includes(:profile).first.address # Returns the address correctly.

So basically accessing these proxied methods require pre-loading to work. But you will want to do that anyway for performance.

# https://gist.github.com/eric1234/10165468
module Proxy
def method_missing method, *args
begin
super
rescue NoMethodError
if __proxy_object.respond_to? method
__proxy_object.__send__ method, *args
else
raise
end
end
end
def respond_to? method, *args
super || __proxy_object.respond_to?(method, *args)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment