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