Saturday, March 31, 2007

[PLUGIN RELEASE] - Facade

Due to a slew of interesting runtime dependencies, we needed to be able to quickly mock out thorny pieces of our architecture.

  • I might not want to run a huge MOA stack to develop software that touches some arcane piece of our application that happens to use messaging.
  • I might want to hit a fully-functional mock (e.g. built in Active Record) instead of hitting live services that we don't own or might cost money.
  • I might want to crank out a naive implementation of a particular data source/sink with the intent of putting in a more 'industrial' solution if the product is a success.

Facade is a library for modeling objects that delegate some or all of their behavior to a surrogate class. It's essentially a DSL for describing a delegation arrangement, as well as a mechanism to bind a particular implementation to those delegator models. It's a little bit of dependency injection mixed with a little bit of IDL.

It also was a relatively enjoyable exercise in Ruby metaprogramming; this would have been quite painful in a few other languages.

Don't worry, it sounds much worse (and Java-esque) than it really is. Actual usage is pretty straightforward and doesn't create a lot of senseless abstraction.

Here's a little self-explanatory peek into the three pieces of the puzzle (the configuration, the facade model, and the implementation model(s).)

Facade model:

class Foo < Facade::Base
backend_class_method :delegated_class_method, :delegated_returns_a_collection_of_self
backend_instance_method :delegated_instance_method
def local_method
puts 'I do stuff that is indepenent of implmentation of delegated stuff'
end
end


Mock delegatee:

class Mock::Foo
def delegated_instance_method
puts 'I performed a mock implementation of my_instance_method'
end
def self.delegated_class_method
puts 'I performed a mock implementation of my_class_method'
end
def self.delegated_returns_a_collection_of_self
return [self.new, self.new]
end
end


Live delegatee:

class Live::Foo
def delegated_instance_method
puts 'I performed a live implementation of my_instance_method'
end
def self.delegated_class_method
puts 'I performed a live implementation of my_class_method'
end
def self.delegated_returns_a_collection_of_self
return [self.new, self.new]
end
end


Config file:

development:
foo:
backed_by: mock
production:
foo:
backed_by: live


The mock/live distinction isn't important--you could just as well have two mock implementations or three live implementations. It's just the name of the module that holds the implementation.

The unique thing about Facade (vis-a-vis other mocking solutions) is its autoboxing support. This allows delegatees to return instances of the delegator without even being aware that its a delegatee (in fact, the delegatee may be a class that you didn't even write.) Whenever instances of the delegatee are returned from a delegated method, an instance of the delegator is returned to the invoker instead. The reason we chose autoboxing over duck-typing is that you want to keep a bulk of the functionality in the implementation-agnostic facade model, and those methods would not be available if we simply returned references to the delegatee whenever the delegatee returned references of itself.

Example: If model Foo contains lots of AR-agnostic business logic but happens to delegate a couple of methods to its AR-backed delegatee--say, for persistence support--we want the results of those ActiveRecord finders to be instances of Foo < Facade::Base, not instances of ArModels::Foo < ActiveRecord::Base, or else we won't be able to invoke Foo's instance methods on the objects in the collection.

Here's a little console session that illustrates the usage of the models shown above:


epf-lap:/tmp/facade_hello_world $ ruby script/console
Loading development environment.
>> Foo
=> Foo
>> Foo.my_class_method
I performed a mock implementation of my_class_method
=> nil
>> foo = Foo.new
=> #<Foo:0x27bb584 @facade_backend=#<Mock::Foo:0x27bb55c>>
>> foo.my_instance_method
I performed a mock implementation of my_instance_method
=> nil

epf-lap:/tmp/facade_hello_world $ ruby script/console
Loading development environment.
>> foo = Foo.new
=> #<Foo:0x27c370c @facade_backend=#<Mock::Foo:0x27bf1c0>>
>> foo.delegated_instance_method
I performed a mock implementation of my_instance_method
=> nil
>> foo.local_method
I do stuff that is indepenent of implmentation of delegated stuff
=> nil
>> Foo.delegated_class_method
I performed a mock implementation of my_class_method
=> nil
>> Foo.delegated_returns_a_collection_of_self
=> [#<Foo:0x27ac64c @facade_backend=#<Mock::Foo:0x27ac728 @my_facade_model=#<Foo:0x27ac64c ...>>>, #<Foo:0x27ac50c @facade_backend=#<Mock::Foo:0x27ac714 @my_facade_model=#<Foo:0x27ac50c ...>>>]
>> ret_foo = Foo.delegated_returns_a_collection_of_self.first
=> #<Foo:0x279c454 @facade_backend=#<Mock::Foo:0x279cbe8 @my_facade_model=#<Foo:0x279c454 ...>>>
>> ret_foo.class
=> Foo
>> ret_foo.delegated_instance_method
I performed a mock implementation of my_instance_method
=> nil
>> ret_foo.local_method
I do stuff that is indepenent of implmentation of delegated stuff
=> nil


epf-lap:/tmp/facade_hello_world $ ruby script/console production
Loading production environment.
>> # notice it's using the other implementation now that we're in prod mode
>> Foo.delegated_class_method
I performed a live implementation of my_class_method
=> nil
>> # and you can dynamically change the implementation, complete with autobox
?> Foo.reset_backend(Mock::Foo)
=> Mock::Foo
>> Foo.delegated_class_method
I performed a mock implementation of my_class_method
=> nil
>> Foo.delegated_returns_a_collection_of_self.first.class
=> Foo
>> Foo.delegated_returns_a_collection_of_self.first.delegated_instance_method
I performed a mock implementation of my_instance_method


Check out the README for information on installation (it's simple!) and getting started.

Readme: svn cat \
svn://rubyforge.org/var/svn/facade/trunk/vendor/plugins/facade/README
Repository: svn checkout \
svn://rubyforge.org/var/svn/facade/trunk/vendor/plugins/facade
Rubyforge: http://rubyforge.org/projects/facade/

Installation:

ruby script/plugin install \
svn://rubyforge.org/var/svn/facade/trunk/vendor/plugins/facade

ruby script/plugin install \
svn://rubyforge.org/var/svn/config-loader/trunk/vendor/plugins/configuration_loader

1 comment:

Anonymous said...

Im not sure if Im reading in to the code right but would it work where you would normally _like_ to use CTI (Class Table Inheritance)?