Consider the Strategy design pattern, which allows encapsulation of an algorithm and allows users to vary them independent of the context. There is nothing object-oriented about a Strategy design pattern. It so happens that the GOF book describes it in the context of an object-oriented language, C++. Stated succinctly, the design pattern espouses yet another best practice of software design, separation of concerns, by decoupling the implementation of an algorithm from the context.
In Java, the Strategy design pattern is implemented through the composition of a polymorphic hierarchy. The strategy is composed into the context through an interface, which can support multiple implementations transparently. Here is an example from a Payroll application, where the algorithm for calculation of a salary is encapsulated into a strategy :
class SalaryComputation {
// injected through dependency injection
private SalaryComputationStrategy strategy;
//..
//..
public BigDecimal compute(..) {
//..
//..
return strategy.compute(..);
}
//..
}
Here SalaryComputationStrategy is an interface which can have multiple implementations. Typically in a Java project, the implementations are injected transparently through a DI container like Spring or Guice.
interface SalaryComputationStrategy {
BigDecimal compute(..);
}
class DefaultSalaryComputationStrategy
implements SalaryComputationStrategy {
//.. implementation
}
class SpecialSalaryComputationStrategy
implements SalaryComputationStrategy {
//.. implementation
}
One of the immediate consequences of the above implementation in Java is the number of classes and the inheritance hierarchies involved in the implementation. Looks like an accidental complexity in implementation, but this is quite a normal way of dealing with a nail when all you have is a hammer.
How do you encapsulate an algorithm and ensure flexible swapping of implementations in a language like Ruby that offers powerful syntax extensibility ? Remember, Ruby is also object oriented, and the above Java implementation can be translated with almost no effort using equivalent Ruby syntaxes. And we have a Strategy implementation in Ruby. Can we do better ?
Abstraction Abstraction!
Abstraction is the key - when you program in a language, always choose the best form of abstraction that suits the problem you are modeling. In a strategy design pattern, all you are encapsulating is an algorithm, which is, by nature a functional artifact. The very fact that Java or C++ does not support higher order functions (anonymous inner classes and function pointers are for the destitutes) had forced us to use encapsulating objects as holders of algorithms. And that led to the plethora of class hierarchies in the Java implementation. Ruby supports higher order functions in the form of blocks and coroutine support in the form of yields - keep reading and hope for a better level of abstraction support.
class SalaryComputation
def compute
## fetch basic, allowances
@basic = ..
@allowance = ..
## fixed logic goes here
## coroutine call for the specific algorithm
yield(@basic, @allowance)
end
##
end
and the algorithm can be injected inline by the client through a Ruby block :
SalaryComputation.new.compute {
|b, a|
tax = (b + a) * 0.2
b + a - tax
}
and we do not have to define a single extra class for the strategy. The strategy is nicely embedded inline at the caller's site. Another client asking to use a different algorithm can supply a different block at her call site.
What if I want to use the same default algorithm for different clients, yet keeping the flexibility of plugging in multiple implementations ? DRY it up within a Ruby Module and use the power of Ruby mixins :
module DefaultStrategy
def do_compute(basic, allowances)
tax = basic * 0.3
basic + allowances - tax
end
end
class SalaryComputation
include DefaultStrategy ## mixin
def initialize basic
@basic = basic
@allowances = basic * 0.5
end
def compute
do_compute(@basic, @hra)
end
end
And for some places where I would like to inject a special strategy, define another Module for the same and extend the SalaryComputation class during runtime :
module SpecialStrategy
def do_compute(basic, allowances)
tax = basic * 0.2
basic + allowances - tax
end
end
s = SalaryComputation.new(100)
s.extend SpecialStrategy
puts s.compute
Handling Fine Grained Variations
Suppose a part of the algorithm is fixed, while the computation of the tax only varies. The typical way to handle this in Java will be through the Template Method pattern. When designing the same in Ruby, the mechanism melds nicely into the language syntax :
def compute
@basic + @allowances - yield(@basic)
end
and the identity of the pattern disappears within the language. Ruby supports fine grained variations within algorithms through powerful language syntax, which can only be done using extra classes in Java.
And finally ..
Keep an eye on the Ruby open classes feature. You can rip open any class and make changes either at the class level or at a single instance level. This feature offers the most coarse grained way to implement strategies in Ruby.
## open up an existing class
class SalaryComputation
## alias for old method
## you may need it also
alias :compute_old :compute
def compute
## new strategy
end
end
So, how many ways does Ruby offer to implement your Strategy Design Pattern ?
2 comments:
Really, there should be a great way to achieve this combining dynamic mixins (i.e., mixins that are done at runtime) and some instance data. So you'll maybe instantiate your object with an @strategy or @type instance variable (or, in the case of a database, provide it), and that will trigger something like an instance-level include (using class << self) that includes the appropriate strategy. Add a simple strategy= method and encapsulate it all in one place.
[there should be a great way to achieve this combining dynamic mixins]
Sure .. Ruby gives you multiple options of implementation for almost everything. Here is one with dynamic mixin :
class SalaryCompute
def self.strategy *props
props.each do |prop|
self.class_eval <<-EOS
include #{prop}
EOS
end
end
strategy :DefaultStrategy
def initialize basic
@basic = basic
@hra = basic * 0.5
end
def compute
do_compute(@basic, @hra)
end
end
puts SalaryCompute.new(100).compute
And, in fact you can take the strategy method out of this class and have it somewhere as as a dynamic strategy method.
Post a Comment