I started playing this evening with the Strategy pattern. The most obvious implementation of the strategy pattern in Ruby is just using modules, like so:
module StrategyWithMixin class Context < Strategy::Context def execute raise 'No default strategy' end end module A def execute arg_one + arg_two end end module B def execute arg_one - arg_two end end end context.extend StrategyWithMixin::A context.execute
It's probably the simplest implementation of the strategy pattern in Ruby. I also came up with an alternate implementation that looks like:
module StrategyWithBlock class Context < Strategy::Context def initialize(a, b) super @strategy = DEFAULT_PROC end def set_strategy(proc) @strategy = proc end def execute self.instance_eval &@strategy end end DEFAULT_PROC = Proc.new do raise 'No default strategy' end A = Proc.new do arg_one + arg_two end B = Proc.new do arg_one - arg_two end end context.set_strategy StrategyWithBlock::B context.execute
Not quite as elegant, but only a single function is being added here, whereas the Mixin has to add the entire module's worth of functions. Not a problem in this case, but it could be in some. A little benchmarking showed that the block implementation was several times faster on JRuby 1.9.
After that I started digging around on the internet and found an implementation that looked something like this:
module StrategyWithClass class Context < Strategy::Context def initialize(a, b) super @strategy = DefaultStrategy.new self end def set_strategy(strategy_class) @strategy = strategy_class.new self end def execute @strategy.execute end end class DefaultStrategy def initialize(obj) @__context_obj__ = obj end def execute raise 'No default strategy' end end class A < DefaultStrategy def execute @__context_obj__.arg_one + @__context_obj__.arg_two end end class B < DefaultStrategy def execute @__context_obj__.arg_one - @__context_obj__.arg_two end end end context.set_strategy StrategyWithClass::A context.execute
This one is actually closer to what would be seen in a Java implementation - the strategy behavior is encapsulated in a class. Surprisingly, this was the fastest implementation yet - it seems like the context switch in the block implementation is slightly slower than making the direct call.
This implementation has one significant advantage over the previous two: if the strategy has state, this encapsulates that state inside the strategy object itself. It's easy to imagine a strategy with cached computations, or a strategy that depends on previous executions of the strategy. This would keep all of that complex behavior encapsulated into the strategy object where it belongs.
The downside is that it isn't very attractive. The execute implementation is littered with ugly calls to the context object. If this method had been written inside of the context class, it would require some significant refactoring to move it into the strategy class. There's got to be a better way.
Of course there is, or this wouldn't be a very interesting blog post. The answer is to delegate to the context object, which looks something like this:
module StrategyWithDelegate class Context < Strategy::Context def initialize(a, b) super @strategy = DefaultStrategy.new(self) end def set_strategy(strategy_class) @strategy = strategy_class.new self end def execute @strategy.execute end end require 'delegate' class DefaultStrategy < SimpleDelegator def initialize(obj) super(obj) end def execute raise 'No default strategy' end end class A < DefaultStrategy def execute arg_one + arg_two end end class B < DefaultStrategy def execute arg_one - arg_two end end end context.set_strategy StrategyWithDelegate::B context.execute
Now we're talking. The strategy implementations can work directly off the instance variables of the context, which looks much more elegant than the previous implementation. The downside is that it is significantly slower - the delegation takes its toll on the benchmarks. In most cases I would prefer the readable version as long as it did not completely destroy performance.
And finally, the benchmarks are below:
user system total real Strategy with mixin 0.307000 0.000000 0.307000 ( 0.307000) Strategy with block 0.087000 0.000000 0.087000 ( 0.087000) Strategy with class 0.072000 0.000000 0.072000 ( 0.072000) Strategy with delegate 0.435000 0.000000 0.435000 ( 0.434000)
No comments:
Post a Comment