N
Nathan Weston
Someone recently mentioned the idea of using Modules to implement the
Decorator pattern (though I can't find the post now, so maybe I
imagined it).
It got me thinking and today I sat down to see if I could do it.
I ended up with a module Decorator,that can be included into another
module to turn it into a decorator. You can then extend an object with
the decorator to decorate it. The next part is that methods defined in
the decorator can use a call_orig method to call to original method
that they are replacing. For example:
module Test
include Decorator
def foo
print "Test#foo\n"
Test.call_orig(self, :foo)
end
end
class Foo
def foo
print "Foo#foo\n"
end
end
f = Foo.new
f.extend(Test)
f.foo
prints:
Test#foo
Foo#foo
It also supports multiple layers of decorators: ie
f.extend(Deco1)
f.extend(Deco2)
and call_orig will still do the right thing.
This approach avoids 3 problems with using delegates to implement
decorator:
Delegates can't be marshalled without some extra work
Delegates can confuse ==
When using delegates, you have to be careful about passing around
references to self, as you might end up with the inner, undecorated
object, where you wanted a decorator.
And here's the code, so you can all find my mistakes:
module Decorator
def Decorator.append_features(mod)
super
#Add these as module (not instance) methods,
#to the module we are extending.
#We can't just define them as Decorator.*, because they won't be
#inherited in the way we want.
class << mod
def orig_name(name)
return "#{name}_#{self.name}"
end
#Create an alias to one of obj's methods, so that
#we can wrap it with a decorator method
def decorate(obj, name)
#We have to eval string to avoid scope issues:
#class << obj
# alias_methodorig_name(name), :name) <-- These variables
# are substituted through string expansion
#end
eval("class << obj\nalias_method#{orig_name(name)},
:#{name})\nend")
end
def extend_object(obj)
#If the extended object has any methods that we are
#going to redefine, make aliases to the originals
#so we can still call them.
instance_methods(false).each { |m|
if obj.respond_to?(m)
decorate(obj, m)
end
}
super
end
#Call the original definition of name on object
def call_orig(obj, name, *args)
obj.send(orig_name(name), *args)
end
end
end
end
Decorator pattern (though I can't find the post now, so maybe I
imagined it).
It got me thinking and today I sat down to see if I could do it.
I ended up with a module Decorator,that can be included into another
module to turn it into a decorator. You can then extend an object with
the decorator to decorate it. The next part is that methods defined in
the decorator can use a call_orig method to call to original method
that they are replacing. For example:
module Test
include Decorator
def foo
print "Test#foo\n"
Test.call_orig(self, :foo)
end
end
class Foo
def foo
print "Foo#foo\n"
end
end
f = Foo.new
f.extend(Test)
f.foo
prints:
Test#foo
Foo#foo
It also supports multiple layers of decorators: ie
f.extend(Deco1)
f.extend(Deco2)
and call_orig will still do the right thing.
This approach avoids 3 problems with using delegates to implement
decorator:
Delegates can't be marshalled without some extra work
Delegates can confuse ==
When using delegates, you have to be careful about passing around
references to self, as you might end up with the inner, undecorated
object, where you wanted a decorator.
And here's the code, so you can all find my mistakes:
module Decorator
def Decorator.append_features(mod)
super
#Add these as module (not instance) methods,
#to the module we are extending.
#We can't just define them as Decorator.*, because they won't be
#inherited in the way we want.
class << mod
def orig_name(name)
return "#{name}_#{self.name}"
end
#Create an alias to one of obj's methods, so that
#we can wrap it with a decorator method
def decorate(obj, name)
#We have to eval string to avoid scope issues:
#class << obj
# alias_methodorig_name(name), :name) <-- These variables
# are substituted through string expansion
#end
eval("class << obj\nalias_method#{orig_name(name)},
:#{name})\nend")
end
def extend_object(obj)
#If the extended object has any methods that we are
#going to redefine, make aliases to the originals
#so we can still call them.
instance_methods(false).each { |m|
if obj.respond_to?(m)
decorate(obj, m)
end
}
super
end
#Call the original definition of name on object
def call_orig(obj, name, *args)
obj.send(orig_name(name), *args)
end
end
end
end