osbridge/openconferenceware

View on GitHub
lib/defer_proxy.rb

Summary

Maintainability
A
0 mins
Test Coverage
# = 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