proxeze

A basic proxy/delegate framework for Ruby that will allow you to wrap any object with a proxy instance. For more information about the Proxy and Delegate patterns, check out en.wikipedia.org/wiki/Proxy_pattern and en.wikipedia.org/wiki/Delegation_pattern respectively.

Details

When Proxeze proxies an object it creates a delegate class under its namespace (using Ruby’s built-in Delegate class) that mirrors your class. It overrides your class’ #new method so that when you create instances of your class you get back the proxy object instead. In this way, you don’t have to change the way you instantiate your objects, but you get the benefit of the proxy pattern. When proxying a class Proxeze will also proxy most class methods (see Proxying Classes below).

How does that help me?

Examples tell it best… In this example we have a tree structure of Categories. At some point during the execution of our program we want to hide certain categories in that tree, without adding extra behavior to Category, and without changing the state of the Category instances themselves. Enter the Proxy.

class Category
  attr_accessor :categories
  attr_reader   :name
  def initialize name
    @name = name
    @categories = []
  end
end

# now every time we instantiate Category, we
# will get back a Proxeze::Category instance
Proxeze.proxy Category

# Controls the visibility of the categories
module Visibility
  def visible?
    @visible = true if @visible.nil?
    @visible
  end

  def be_invisible!
    @visible = false
    categories.each {|e| e.be_invisible!}
    self
  end

  def be_visible!
    @visible = true
    categories.each {|e| e.be_visible!}
    self
  end

  Proxeze::Category.send :include, self
end

# instantiate Category objects as we normally would
c1            = Category.new('1')
c1.categories = [Category.new('2'), Category.new('3')]
c2            = c1.categories.first
c2.categories = [Category.new('4'), Category.new('5'), Category.new('6')]
c6            = c2.categories.last
c6.categories = [Category.new('7'), Category.new('8')]

c6.be_invisible!

# before we hide c6 and its children   |    after hiding c6
# we have a tree like this:            |    our tree becomes:
# ------------------------------------ + --------------------
#                c1                    +            c1
#             ___||___                 +         ___||___
#             ||    ||                 +         ||    ||
#             c2    c3                 +         c2    c3
#         ____||____                   +     ____||____
#         ||  ||  ||                   +     ||      ||
#         c4  c5  c6                   +     c4      c5
#              ___||__                 +
#              ||    |                 +
#              c7    c8                +
#                                      |

So, we’ve added behavior to the proxied objects, but not the Category objects themselves.

You can also proxy an object without overriding the #new method on its class. This is particularly useful if you have an object that you want to proxy, but you aren’t able (or don’t want) to proxy every instance of the class.

In this example, you can’t define Fixnum>>#new, so if you need a proxy for Fixnums here’s how you do it

p = Proxeze.for(1)
p.class             # Proxeze::Fixnum
p.__getobj__.class  # Fixnum

Here, we don’t want all instances of A being proxied, but we do need a proxy for a particular instance of it:

class A
  attr_accessor :foo
  def initialize; @foo = 1; end
end

p = Proxeze.for(A.new)
p.class             # => Proxeze::A 
p.__getobj__.class  # => A

Of course, you can proxy the class and all subsequent instances even after you’ve proxied a specific instance:

Proxeze.proxeze A
a = A.new
a.class             # => Proxeze::A 
a.__getobj__.class  # => A

Proxying Classes

As we said before when you proxy a class, generally speaking, the #new method will be aliased so that all subsequent instances of the class will be proxied instances. What is the #new method? It’s just an instance method of your class. What about all of the other instance methods of the class? In large part, they are available also, though there are mechanisms for making them unavailable. Consider this scenario:

class A
  def self.foo; 1; end
  def self.bar; 1; end
end

I can tell Proxeze to allow for the invocation of the #foo method, but not the #bar method like this:

Proxeze.proxy A, :exclude_class_methods => [:bar]

If you don’t specify the :exclude_class_methods option, all class methods defined in your class will be available.

The special case to this is that class methods defined in Object are not proxied, and there are a few class methods in the Delegate framework that are excluded. What if you’ve overridden a class method that is already defined in Object, like #hash? Well you can include that by sending the :include_class_methods option:

class A
  def self.hash; 17; end
end
Proxeze.proxy A, :include_class_methods => [:hash]

Note: I don’t recommend overriding the #hash method on your class, this serves only as an example.

Method Interceptions

Proxeze has the ability to surround instance method calls with before and after callbacks. This support was lifted straight from proxy_machine (github.com/tulios/proxy_machine). Unlike proxy_machine, all callbacks receive the same set of parameters: a reference to the object, the arguments passed, the symbol of the called method, and the result of execution (this result could be nil, and necessarily will be nil in before and before_all interceptions).

Defining callbacks at the method level

before

p = Proxeze.for [0, 1, 2, 3] do
  before :reverse do |obj, args, mid, result|
    obj << obj.length
  end
end  

p.reverse # => [4, 3, 2, 1, 0]

after

p = Proxeze.for [4, 2, 3] do
  after :reverse do |obj, args, mid, result|
    result.sort
  end
end

p.reverse => [4, 3, 2] # We reordered the list

Defining callbacks for all method calls

before_all

logged = nil
p = Proxeze.for [0, 1, 2, 3] do
  before_all do |obj, args, method, result|
    logged = "before #{method} on #{obj.inspect} with args[#{args.inspect}]"
  end
end
p.reverse # => [3, 2, 1, 0]
logged    # => before reverse on [0, 1, 2, 3] with args[[]]

p.size => 4
logged    # => before size on [0, 1, 2, 3] with args[[]]

p.unshift 9 # => [9, 0, 1, 2, 3]
logged      # => before unshift on [0, 1, 2, 3] with args[[9]]

after_all

logged = nil
p = Proxeze.for [1, 2, 3] do
  after_all do |obj, args, method, result|
    logged = "after #{method} on #{obj.inspect} with args[#{args.inspect}], result is now [#{result}]"
    result
  end
end
p.reverse # => [3, 2, 1]
logged    # => after reverse on [1, 2, 3] with args[[]], result is now [[3, 2, 1]]

p.size    # => 3
logged    # => after size on [1, 2, 3] with args[[]], result is now [3]

Registering a class to perform a callback

The initializer will receive an extra parameter, the type of the callback (one of #before, #before_all, #after, or #after_all). In addition it also receives the object, the arguments passed to the method, the method called, and a result (which is populated for #after and #after_all hooks). You will also need to define a #call method. Proxeze will create a new instance of the class every time it needs to use it.

# Example of class
class SortPerformer
  def initialize callback_type, object, args = nil, method = nil, result = nil
    @object = object; @args = args; @method = method; @result = result
  end

  def call; @object.sort! end
end

p = Proxeze.for [1, 4, 2, 3] do
  after :reverse, SortPerformer
end

p.reverse => [1, 2, 3, 4]

Supported Ruby versions

This code has been tested on 1.8.7, 1.9.2, and JRuby 1.5.6. I haven’t bothered to test it on anything else, but I strongly suspect it will work just fine on any Ruby implementation greater than 1.8.6.

If you run into problems, feel free to open an issue at github.com/jacaetevha/proxeze/issues, or you can fix it yourself (see “Contributing to proxeze” below).

Contributing to proxeze

  • Check out the latest master to make sure the feature hasn’t been implemented or the bug hasn’t been fixed yet

  • Check out the issue tracker to make sure someone already hasn’t requested it and/or contributed it

  • Fork the project

  • Start a feature/bugfix branch

  • Commit and push until you are happy with your contribution

  • Make sure to add tests for it. This is important so I don’t break it in a future version unintentionally.

  • Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.

Copyright © 2011 Jason Rogers. See LICENSE.txt for further details.