Consistent interfaces, contrived examples and define_method for instances

Whenever I see code which does two (or more) things in sequence in order to achieve one objective, I feel obliged to try to figure out a better way. It has to do with the fact that I'm pretty compulsive - I like to have to use just one interface to achieve one logical objective. I also check the lock on my front door several times every night before admitting that it is, in fact, locked.

I came across one such example in the Pickaxe when I was reading up on threads in Ruby, specifically the one that talks about synchronising a method on a single instance using MonitorMixin(p 137). Needless to say my compulsions asserted themselves, threading went out of the window and I started looking for a cleaner way to implement the example. I'm duplicating it below to save you the trouble of looking it up:
require 'monitor'
class Counter
attr_reader :count
def initialize
@count = 0
end
def tick
@count += 1
end
end
c = Counter.new
c.extend(MonitorMixin)
t1 = Thread.new { 10000.times { c.synchronize { c.tick } } }
t2 = Thread.new { 10000.times { c.synchronize { c.tick } } }
t1.join; t2.join
The authors conclude the example with this observation:
Here, because class Counter doesn’t know it is a monitor at the time it’s defined, we have to perform the synchronization externally (in this case by wrapping the calls to c.tick). This is clearly a tad dangerous: if some other code calls tick but doesn’t realize that synchronization is required, we’re back in the same mess we started with.
We'd like to fix this problem and have our code read like
c = Counter.new
c.extend(ThreadSafeInstance).make_safe(:tick)
t1 = Thread.new { 10000.times { c.tick }}
.

Before we get started trying to sort this out, I should point out that situations where you need to extend individual instances and then decorate methods on them occur rarely. You're likely to find an easier solution if you re-examine your architecture and try to identify precisely why you need to mess with instances. The example above is clearly contrived and is almost certainly a code smell. What we're going to do to fix this contrived example is what a friend of mine (Srushti, the chap behind XStream.net) called a 'freaky cool' solution - it's cool, but if you need it in real life then you probably have some design issues in your architecture. We can consider it an exercise in metaprogramming, but not much more.

This problem calls for Rails' alias_method_chain style method decoration - a means by which we can decorate tick() so that we can have two new methods, tick_without_synchronization() and tick_with_synchronization(). In Java or C#, you could achieve a similar effect by using dynamic proxies. The catch is, in this case we only want to decorate a single method on a single instance whereas alias_method_chain and dynamic proxies work on classes and consequentially modify the perceived behaviour of all instances of a class - something we don't want. Instead, we need to implement alias_method_chain ourselves, but in a way which allows us to modify just a single instance.

This shouldn't be a big deal - we can use Ruby's alias_method, right? No such luck. alias_method works at a module level; since Class is a subclass of Module in Ruby, this method modifies all instances of a class that it acts on. The same holds good for define_method. This seems problematic - if we had define_method for instances, we can implement our own alias_method and in turn alias_method_chain.

The only way I could find which allowed me to programmatically add methods to an instance was by means of extending it with a module. That's what I ultimately did - I created an anonymous module, defined the method within it and then extended my instance with that module. Here's the RSpec specification for define_method followed by the code.
describe 'An instance of', Object do
before(:each) do
@o = Object.new
end

it 'should know how to define a new method' do
@o.should_not respond_to(:ooga)
@o.define_method(:ooga){}
@o.should respond_to(:ooga)
end

it 'should not affect other instances when defining a new method' do
@o.define_method(:ooga){}
Object.new.should_not respond_to(:ooga)
end

it 'should be able to define a new method which accepts parameters' do
@o.define_method(:echo){|*args| args}
@o.echo(1,2,3,4,5).should == [1,2,3,4,5]
end
end

class Object 
def define_method(method_name, &block)
self.extend(
Module.new{
define_method(method_name, block)
}
)
end
end


Now for alias_method - spec, followed by implementation
describe 'An instance of', Object do 
before(:each) do
@o = Object.new
end

it 'should be able to alias a method' do
@o.alias_method(:new_to_s, :to_s)
@o.should respond_to(:new_to_s)
end

it 'should have aliased methods respond like the originals' do
@o.alias_method(:new_to_s, :to_s)
@o.new_to_s.should == @o.to_s
end

it 'should ensure that aliased methods are copies of, not references to the originals' do
@o.to_s.should_not be_nil
@o.alias_method(:new_to_s, :to_s)

@o.define_method(:to_s){}
@o.to_s.should be_nil

@o.new_to_s.should_not be_nil
end
end

class Object
def alias_method(new_id, original_id)
original = self.method(original_id).to_proc
define_method(new_id){|*args| original.call(*args)}
end
end

Let's apply these two new methods to the original problem:
require 'monitor'

class Counter
attr_reader :count

def initialize
@count = 0
end
def tick
@count += 1
end
end

module ThreadSafeInstance
def self.extended(instance)
instance.extend(MonitorMixin)
end

def make_safe(method_symbol)
original_method = "_unsafe_#{method_symbol}_"
alias_method(original_method, method_symbol)
define_method(method_symbol){|*args|
self.synchronize{self.send(original_method)}
}
self
end
end

c = Counter.new
c.extend(ThreadSafeInstance).make_safe(:tick)
t1 = Thread.new { 10000.times { c.tick }}
t2 = Thread.new { 10000.times { c.tick }}
t1.join; t2.join

There! The interface is much cleaner now - client code which consumes instance c needn't be aware of the fact that tick() needs to be synchronised. They need only be concerned with the single objective of tick(), that it allows us to increment Counter.

Incidentally, we cannot define methods which accept blocks using these methods we just created, a limitation of the original define_method which our implementation uses.
Post a Comment