Contents |
Strategy
The strategy pattern provides a way for you to easily change the way an object behaves at runtime. It essentially allows you to "plug" some behaviour into an object.
For a CFML implementation there are two kinds of objects in this pattern; a Context and a Strategy.
The Context object is our main object; when we need a certain task done we ask the Context object to do it. Contained within the Context object is our Strategy object. When we ask the Context to perform a task, it asks the Strategy object to perform either some or all of the work.
If we were to replace the Strategy object with a different Strategy object then this would then affect the overall behaviour of the Context.
In other words, we may initially start out having a Context with one kind of Strategy object:
Calling context.doSomething() performs some simple behaviour.
But we may then change the Strategy object to something a more complicated.
Calling context.doSomething() now performs a more complex behaviour.
How to provide the Strategy to the Context?
There area number of ways to provide the Strategy to the Context, but some common techniques are:
1) Provide the Strategy to the Context's init() function:
<cffunction name="init" output="false"> <cfargument name="strategy" required="true"> <cfset variables.strategy = arguments.strategy> </cffunction>
2) Provide the Strategy via a setter
<cffunction name="setStrategy" output="false"> <cfargument name="strategy" required="true"> <cfset variables.strategy = arguments.strategy> </cffunction>
3) Provide the Strategy directly to the function that needs it
<cffunction name="doSomethingWithThisStrategy" output="false"> <cfargument name="strategy" required="true"> ... code that uses the strategy ... </cffunction>
Example: Tank game
Let's start with a simple game that demonstrates the idea of the Strategy pattern. In this game you have two tanks that move around a playing field and try to shoot each other. Your tank objects may look something like this:
Our simple tank object is responsible for both shooting and moving around.
<cfcomponent name="Tank" output="false"> ... initialisation code here ... <cffunction name="shoot" output="false"> ... code to perform shooting here ... </cffunction> <cffunction name="move" output="false"> ... code to perform moving here ... </cffunction> </cfcomponent>
This is great, but the game would be more interesting if you could collect upgrades that either increased the damage you inflict or increased your speed.
Let's assume we can upgrade two aspects of our tank; the turret which affects the damage we inflict, and the chassis which determines how our tank moves. Let's revise our tank object to allow the tank turrets and chassis to be upgraded via setTurret() and setChassis() functions.
Now that we have a turret and a chassis, we can pass on the responsibility of shooting and moving to these new tank parts.
<cfcomponent name="Tank" output="false"> ... initialisation code here ... <cffunction name="setTurret" output="false"> <cfargument name="turret" required="true"> <cfset variables.turret = arguments.turret> </cffunction> <cffunction name="setChassis" output="false"> <cfargument name="chassis" required="true"> <cfset variables.chassis = arguments.chassis> </cffunction> <!--- When shooting we ask the turret to actually do the work ---> <cffunction name="shoot" output="false"> <cfset variables.turret.shoot()> </cffunction> <!--- When moving we ask the chassis to do the work ---> <cffunction name="move" output="false"> <cfset variables.chassis.move()> </cffunction> </cfcomponent>
The setTurret() and setChassis() functions make it easy for us to change the shooting and movement behaviour of the tank.
For example, we may start with a basic turret that fires basic shells, but later upgrade to a turbo turret that fires shells at a faster rate. We might even upgrade to an anti-gravity chassis which allows our tank to fly.
Let's take a look at some example code that demonstrates how our new tank object might be used. First we need to create a couple of different turrets and chassis.
<!--- Couple of turrets ---> <cfset basicTurret = createObject("component","BasicTurret").init()> <cfset turboTurret = createObject("component","TurboTurret").init()> <!--- Couple of chassis ---> <cfset basicChassis = createObject("component","BasicChassis").init()> <cfset turboChassis = createObject("component","TurboChassis").init()>
Now let's put our tank into action.
<!--- Create our tank ---> <cfset tank = createObject("component","Tank").init()> <!--- Equip it with a basic turret and chassis ---> <cfset tank.setTurret(basicTurret)> <cfset tank.setChassis(basicChassis)> <!--- Move a bit and shoot. We've got a pretty lowly tank here ---> <cfset tank.move()> <cfset tank.shoot()> <!--- Now equip the tank with a turbo turret and chassis ---> <cfset tank.setTurret(turboTurret)> <cfset tank.setChassis(turboChassis)> <!--- Move a bit and shoot. Now we've got speedier tank shooting at twice the speed now! ---> <cfset tank.move()> <cfset tank.shoot()>
Example: Payment gateways
When making an online payment you may have a choice of which payment gateway to use; you may choose to use PayPal, Google Checkout or perhaps a local payment gateway provider. The Strategy pattern can be used to assign which payment gateway is used to handle the payment.
For this example, suppose you are developing a membership system and when a person registers or renews they are required to make a payment.
We may have a Membership object that knows the amount required for the transaction.
We also have a Payment Gateway object that knows how to process a payment. Let's assume that in this instance we need to use a PayPal payment gateway.
When we call purchase() on the membership object, it asks the gateway object to perform some of the work.
<cffunction name="purchase" output="false"> <cfargument name="gateway" required="true"> <cfargument name="cardNum" required="true"> <cfargument name="expiryYear" required="true"> <cfargument name="expiryMonth" required="true"> <cfset var amount = variables.STANDARD_MEMBERSHIP_FEE> <cfset var results = arguments.gateway.processPayment( arguments.cardNum, arguments.expiryYear, arguments.expiryYear, amount )> ... handle results here ... </cffunction>
The PayPalPaymentGateway object takes care of authenticating with PayPal and passing on any merchant details.
If we want to swap the payment gateway to be the GoogleCheckout provider, then we would create a GoogleCheckoutPaymentGateway that also implements a processPayment() function with the same parameters.
So let's take a look at how we might use our membership object.
<!--- Create one of each of our payment gateways and put them into a lookup struct ---> <cfset gateways = {}> <cfset gateways["paypal"] = createObject("component","PayPalPaymentGateway").init( ... paypal parameters here ... )> <cfset gateways["google"] = createObject("component","GoogleCheckoutPaymentGateway").init( ... google parameters here ... )> <cfset gateways["ezypay"] = createObject("component","EzyPayPaymentGateway").init( ... ezpay parameters here ... )> <!---Select the gateway based on an option chosen by the user ---> <cfset gateway = gateways[selectedGateway]> <!--- Purchase the membership ---> <cfset membership.purchase(gateway, cardNum, expiryYear, expiryMonth)>
The final point to notice in this example is that the Payment Gateway is provided at the point of purchase rather than via the init() function or a setter function.
CFML specific alternative to the Strategy Pattern
Within CFML functions may be passed around as variables. This allows an alternative technique for implementing a Strategy pattern; rather than passing Strategy objects to the Context object you can pass Strategy functions to the Context object.
For example, suppose we need to calculate a journey from one location to another. We may provide an option to either travel the quick route or the scenic route. We can create a TravelStrategies object that knows how to calculate the quick or scenic routes.
<cfcomponent name="TravelStrategies" output="false"> <cffunction name="init" output="false"> <cfreturn this> </cffunction> <cffunction name="quick" output="false"> <cfargument name="from" required="true"> <cfargument name="to" required="true"> ... Calculate the quickest route ... <cfreturn route> </cffunction> <cffunction name="scenic" output="false"> <cfargument name="from" required="true"> <cfargument name="to" required="true"> ... Calculate the most scenic route ... <cfreturn route> </cffunction> </cfcomponent>
Then we may have a TravelPlanner that needs to use a travel strategy.
<cfcomponent name="TravelPlanner" output="false"> ... initialisation code etc. ... <cffunction name="setTravelStrategy" output="false"> <cfargument name="travelStrategy" required="true"> <!--- Note that the travelStrategy is a 'function reference' ---> <cfset variables.travelStrategy = arguments.travelStrategy> </cffunction> ... more code ... <cffunction name="getPlan" output="false"> <!--- Call the function stored in the 'travelStrategy' variable ---> <cfset var route = variables.travelStrategy(variables.fromLocation,variables.toLocation)> ... Some work to convert the route into an enhanced trip plan that describes places to visit ... <cfreturn tripPlan> </cffunction> </cfcomponent>
So to use our objects we may write:
<!--- Create our strategies and planner objects ---> <cfset strategies = createObject("component","TravelStrategies").init()> <cfset planner = createObject("component","TravelPlanner").init()> <!--- Try a quick journey first ---> <cfset planner.setTravelStrategy( strategies.quick )> <cfset plan = planner.getPlan()> <!--- Try a scenic journey next ---> <cfset planner.setTravelStrategy( strategies.scenic )> <cfset plan = planner.getPlan()>
The important thing to notice here is that we pass a function into the setTravelStrategy() function rather than an object. Then when we call the getPlan() function is just executes the travelStrategy function without knowledge of which strategy it actually is.
Discussion
See the discussion of this page on the OOCF mailing list
http://groups.google.com.au/group/coldfusionoo/browse_thread/thread/60661c9abbb38ec9
References
Wikipedia - Strategy Pattern
http://en.wikipedia.org/wiki/Strategy_pattern
SideBar
User Login