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'The authors conclude the example with this observation:
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
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 implementationdescribe '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.
3 comments:
hi Sidu,
This is vivek here. I went through your work. We are working on an initiative and would like you to partner with us, if you find it intersting.
Can you contact me at vivek.sp at gmail dot com.
my phone number is 9844431932. you can call/sms me
hello sidu,
a very good post on metaprogramming. thanks,
linh
Post a Comment