lib/defer_proxy.rb
# = DeferProxy
#
# Create a proxy for lazy-loading objects.
#
# == Example for Ruby
#
# In the example below, we create an @array variable that behaves just like an
# Array instance, but is actually a proxy that will return array content. The
# proxy materialies this content the first time that an instance method is
# called on it:
#
# # Create a proxy using either the DeferProxy class or Defer method:
# @array = DeferProxy.new { p "zzz"; sleep 1; p "yawn"; [1,2,3] }
# @array = Defer { p "zzz"; sleep 1; p "yawn"; [1,2,3] }
#
# # Access proxy to materialize results, it will execute slowly:
# @array.size
# # "zzz"
# # "yawn"
# # => 3
#
# # Access proxy again, it will return materialized results immediately:
# @array.size
# # => 3
#
# == Example for Rails
#
# Deferred-instantiation proxies are useful in Rails applications because they
# help avoid slow operations while preserving MVC encapsulation.
#
# In the example below, the +Record+ class has a ::slow_operation method that
# we wish to avoid calling if possible. We create a proxy called @records in
# the #index controller action to describe how to fetch data. The view then
# uses the @records object within a #cache block that captures HTML emitted
# within that scope. If the view finds the fragment cache for this block, then
# the @records proxy object will never be used and the ::slow_operation never
# called, thus making the action much faster.
#
# # Model
# class Record < ActiveRecord::Base
# def self.slow_operation
# puts "Performing slow operation...."
# sleep 3
# puts "Completed slow operation!"
# return [1,2,3]
# end
# end
#
# # Action
# def index
# @records = Defer { Record.slow_operation }
# end
#
# # View
# <% cache "record_index" do %>
# <%= @records.size %>
# <% end %>
class DeferProxy
# In order to proxy the most methods, remove this object's instance methods
# so that calls will be sent to #method_missing for forwarding.
instance_methods.each { |m| undef_method(m) unless m =~ /^__/ || m == :object_id }
# Create proxy that, when called, will return a value by callling the block.
def initialize(&block)
@__defer_proxy_callback = block
end
# Return true if asked if this is a kind of DeferProxy else fallback to
# +kind_of?+ behavior of proxied object.
def kind_of?(klass)
return(klass == DeferProxy || method_missing(:kind_of?, klass))
end
# Materialize the proxied object and call it.
def method_missing(name, *args, &block)
return(__materialize.send(name, *args, &block))
end
# Return the proxied object, materializing it if needed.
def __materialize(*args, &block)
return(@__defer_proxy_value ||= @__defer_proxy_callback.call(*args, &block))
end
end
# Return a DeferProxy instance for the given +block+.
def Defer(&block)
return DeferProxy.new(&block)
end
# Return the content of a value, be it a Defer or not.
def Undefer(value)
return value.kind_of?(DeferProxy) ? value.__materialize : value
end
# Self-test
if __FILE__ == $0
require 'test/unit'
class DeferProxyTest < Test::Unit::TestCase
def test_should_proxy_an_integer
assert_equal(1, Defer{1})
end
def test_should_proxy_an_array
assert_equal([1,2,3], Defer{[1,2,3]}.map{|t| t})
end
def test_should_proxy_an_array_for_use_with_map
assert_equal([1,2,3], Defer{[1,2,3]}.map{|t| t})
end
def test_should_be_able_to_use_kind_of_on_proxied_object
assert(Defer{[1,2,3]}.kind_of?(Array))
end
def test_should_be_able_to_use_kind_of_on_self
assert(Defer{[1,2,3]}.kind_of?(DeferProxy))
end
def test_should_provide_access_to_proxied_object
assert_equal([1,2,3], Undefer(Defer{[1,2,3]}))
end
def test_should_provide_access_to_proxied_object_thats_not_a_defer
assert(Undefer(Defer{[1,2,3]}).respond_to?(:__materialize) == false)
end
end
end