What is a strategy and how do I use it?
The strategy describes one solution to the main problem inherent in writing generic code - how to deal with unforeseen demands for changes in implementation.
How do I recognize where I need a strategy?
The formal definition of the strategy, as given by “Design Patterns, Elements of Reusable Object-Oriented Software” by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides is:
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it
This is a little obscure on first reading so let us look at a common example where a strategy pattern might help.
Consider the problem of calculating the Sales Tax due for a purchase. We can assume that our application knows the item purchased, and the quantity and so (from its pricing information) can determine the value of the purchase. Now we have to apply the Sales Tax.
This is no problem if we happen to be in an environment where Sales Tax is applied at a single rate irrespective of location (as with VAT in the UK for example). The only issue is whether is the item is liable for tax, and if so, what is the currently applicable rate. However, in the USA, Sales Tax is local! The rate applied depends either on where you buy something (or even, in some cases, where the purchaser resides).
For example, the Sales Tax on clothing in our local area is 5.75%, but if we travel 20 miles south the tax base is only 5.25% while if we travel into the next state (only 50 miles away) there is no tax on clothing at all. The same item of clothing ticketed at $29.95 could therefore cost us either $31.67, $31.52 or $29.95 depending on where we buy it. Stating this in more general terms, the tax applicable to the transaction depends upon the context (the specific combination of type of item and the location) in which the transaction takes place. If we were writing code into an application to handle this we might start out with something like this:
DO CASE
CASE lclocale = “Akron”
IF lcItemType = “Clothing”
lnTaxRate = 5.75
ELSE
*** Other items here
ENDIF
CASE lclocale = “Canton”
IF lcItemType = “Clothing”
lnTaxRate = 5.25
ELSE
*** Other items here
ENDIF
CASE lclocale = “Grove City”
IF lcItemType = “Clothing”
lnTaxRate = 0.00
ELSE
*** Other items here
ENDIF
OTHERWISE
*** Apply a Default value
lnTaxRate = 5.50
ENDCASE
We can see immediately what one problem is going to be! What happens when we need to add ‘Cleveland’ to our list of locales, or when Akron, dismayed at the loss of clothing sales to Canton, cuts its tax rate for clothing. We need to change our code with all the concomitant risks involved. Moreover as we increase the number of locations, this code will rapidly become unmanageable. Of course, we could store this information in a table (one column for location, and one column for each item type perhaps) and look it up each time that we needed it. That way we would only have to modify records when data changed, but it could quickly become a very large table indeed and there would be a lot of duplication and redundancy in the data, so even this is not an ideal solution. What we really want our application to do is to be able to pass the ticket price, and context information, to something that will simply tell us what the tax should be. In other words we want to separate the abstraction (calculate the tax due) from the implementation (based on locale).
However, unless we move the code, which re-locates, but does not solve, the problem a simple bridge is not the solution because it only permits a single implementation, and we require multiple implementations. What we need is to be able to decide, at run time, which implementation we require. The strategy pattern allows us to define classes for each situation that we must deal with and then instantiate the appropriate one at run time.
What are the components of a strategy?
A strategy has three essential components, the “abstract strategy” which is the class that defines the interface, and generic functionality, for the “concrete strategies” which are the sub classes that define the various possible implementations. The third component, the “context”, is responsible for managing the reference to the current implementation. The strategy is initiated by a request for action from the client.
You can think of a strategy as a ‘dynamic bridge’ in which one end (the context) is static, but the other (the strategy) is being created from among a number possible of variants in order to deliver a specific implementation at a given moment. The essence of the pattern is that the decision as to which sub class should be instantiated at any time depends upon the information derived from the client (i.e. the application).
Typically the pattern lays the responsibility for instantiating the concrete strategy object on the client, which then passes a reference to the context. In order to implement a strategy pattern you need to define the abstract strategy class and as many different specific sub classes as you need. The object, which is going to play the role of the context (in VFP this is typically the form, or parent container), needs to expose an appropriate interface to its potential clients and also requires a property that is used to store the reference to the currently active implementation object.
How do I implement a strategy?
The following example illustrates how we might use a strategy pattern to implement a solution to the Sales Tax issue that was outlined in the preamble to this topic.
The first thing to do is to define an abstract class which has a property for the applicable tax rate. It also has methods for calculating the tax using the passed value and the rate property. We then create as many sub-classes of this definition as we have tax rates to deal – one sub-class for each rate.
Now we need to consider how we decide which of the various sub-classes we need at any given time. In order to determine that, we need to relate the various locations and applicable tax rates (the “locale”) to the appropriate sub class. There are, of course, many ways we could do this and the simplest is just to define the list of locations (i.e. cities) and the rate of tax which applies for each. Since we have a specific sub class for each rate of tax, we can derive the subclass necessary for any specific location from the rate.
While this still needs a table with one record per city, we can reduce the duplication by naming our sub-classes according to the rate of tax they apply. The result would something like this:
City
|
Rate
|
Sub-Class
|
Akron
|
5.75
|
Tax575
|
Canton
|
5.25
|
Tax525
|
Grove City
|
0.00
|
Tax000
|
Kent
|
5.75
|
Tax575
|
In other words we use the applicable rate to identify the subclass we need with the consequence that we do not need the third column in the table – we can infer it directly from the Rate.
So, how do we code for this in a form? Well, it’s pretty simple. All we need is a drop down list of locations based on our City/Rate table. When the city is selected we can ascertain the applicable tax rate. Given that, we can derive the name of the sub class to instantiate, and since all the sub classes derive from a common parent we know that all we have to do is to call the appropriate method on the object, passing the expected parameters – in this case, two parameters – the context key (which, defines the applicable rate ) and the “price” on which the tax is to be calculated. The context key is used to generate the name of the required sub class. If the currently instantiated class is not the right one, we simply instantiate the correct one as follows .
LPARAMETERS tcContext, tnPrice
WITH This
*** Define the name of the strategy object based upon the root class + rate required
lcStrategy = "CNTSTRAT" + PADR( tcContext, 3, '0' )
*** Is it already there
IF ISNULL( .oStrategy ) OR NOT UPPER( .oStrategy.Name ) == lcStrategy
*** We need to create this one
.oStrategy = NEWOBJECT( lcStrategy, 'Ch15.vcx' )
ENDIF
*** Now just call it's CalcTax() method and pass the price
lnTax = .oStrategy.CalcTax( tnPrice )
RETURN lnTax
ENDWITH
While this is one way in which a strategy could be implemented, and it makes sense in the given scenario, other scenarios may require different implementations. For example consider the application of discounts to an order. The order entry form (the context) may need to apply several different kinds of discount to an order:
· Item quantity discounts, applied to a single line item at a time
· Order value discount, applied to the total order value
· Special item discounts or promotions
· Customer discount applied to order value based on the customer
One possible interface would use command buttons (the clients) so that the operator can determine when to apply a specific type of discount. In that scenario it would be entirely appropriate, and much simpler, for each button to be responsible for instantiating the appropriate discount strategy sub class and storing it’s reference to the form property. (Remember that while we can do this sort of thing easily in Visual FoxPro, other languages would have to pass the reference explicitly). The form could then handle the actual calculation and deal with displaying the result so that the code does not have to be duplicated in every button.
Strategy pattern summary
We have seen one possible implementation of the strategy pattern and outlined another. The benefit of the strategy is that it avoids the necessity of having to embed alternative implementations in code allowing us to create separate objects, which share a common interface, for each option and to only instantiate the necessary object at run time. As with other patterns the actual implementation details may vary with circumstances but the pattern itself does not change.
Published Monday, December 18, 2006 1:43 AM by andykr
No comments:
Post a Comment