Close Window

Print Story

Design Patterns in ColdFusion: Strategy Pattern - Part 3 of a Series

For the past couple of issues we have been getting to know how other languages use object-oriented design patterns and how those patterns can be implemented in ColdFusion MX. We jumped right into the Template Method and Iterator patterns and this month we will explore the Strategy pattern.

This month's pattern has some things in common with the Template Method pattern we reviewed in March (CFDJ, Vol. 5, issue 3). The Strategy pattern helps us encapsulate a series of related algorithms, or strategies, and gives us a single interface, known as the context, with which to work with them.

What Is a Strategy Anyway?
As I've mentioned previously, the book Design Patterns: Elements of Reusable Object-Oriented Software is the preeminent work on the subject of design patterns. Its authors, the so-called "Gang of Four," established the general intent of the Strategy pattern as the following:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Okay. For once that is actually fairly easy to understand. We have a number of different calculations from which we want our program to be able to choose the right one on the fly.

In the example you will see that strategies allow us to follow two basic principles of design patterns, which were laid out by the "Gang of Four" in their book. The first, "Favor object composition over class inheritance," encourages object-oriented programmers to rely more on composing new objects (or in our case CFCs) from other objects and not to expect to solve all our code issues by extending existing objects. The second, "Program to an interface, not an implementation," encourages us to program our CFCs to work on common interfaces and not to hard-code too much into one implementation.

In the example, I will attempt to address both of these points. However, if you have not worked with CFCs or know nothing about inheritance or object-oriented programming, then you may get more out of this article if you read the previous articles in this series first.

Strategy Pattern:
When and Why?

So you want to learn design patterns and need to know how to recognize when it is appropriate to use the Strategy pattern? Here are a few questions to ask yourself about possible Strategy candidates:

  • Do you have related CFCs that differ only in their behavior?
  • Do you need to use a varying algorithm or make a different calculation based on parameters?
  • Does an algorithm use data that should be available only to the algorithm?

    We will see an example implementation of the Strategy pattern that includes a "context" CFC that encapsulates the interchangeable strategies. This also provides a way to call a polymorphic function, which will exhibit one of many different behaviors. The Strategy pattern can also avoid exposing complex, algorithm-specific data structures.

    The real question may be why should we use a Strategy pattern instead of a standard switch-case statement. Here are a couple of reasons:

  • The code of the strategy's context CFC, or any of the other strategies, doesn't have to be changed when a new strategy is added.
  • Coding a single Strategy, although polymorphic, is very simple. <cfswitch><cfcase> statements, especially when multiple switch-cases are nested inside each other, can become extremely complex and result in the need to evaluate many variables to determine what should be processed.

    Strategy Pattern in the Real World
    Recently I was contracted to build an online ticketing application for TicketLeap.com using ColdFusion MX and Flash MX. one of the main tricky issues was the complex and varied calculations needed for pricing tickets. Tickets have a standard price and often have discounted pricing for children or senior citizens. Tickets also have a variable service charge and may charge sales tax in some cases. My first thought was to use design patterns and the Strategy pattern.

    In our real-world but limited example, taken from the TicketLeap.com project, each Strategy will be its own object or CFC. This isn't unusual because in object-oriented programming programmers tend to think of everything as an object. This isn't 100% necessary but it gives us the flexibility to add additional CFC strategies that implement the same interface without changing a single line of existing code.

    We are then establishing an interface with which to work with strategies and then programming to that interface as opposed to the specific file we are working on. This promotes polymorphism (the ability to override methods in derived CFCs) and code reuse, which are always good things to think about when programming CFCs.

    First we start with the top-level AbstractStrategy.cfc, which defines the interface that all future Strategy CFCs will exhibit. Here we have a single method called Execute(), which in this case causes a <cfabort> to be processed since this CFC is abstract and should never itself be instantiated.

    <cfcomponent displayname="abstractStrategy">
    <cffunction name="Execute" access="public">
    <cfabort showerror="This Method is Abstract
    and needs to be overridden">
    </cffunction>
    </cfcomponent>

    Next we have the derived PricingStra-tegy.cfc, which has an Execute() method that takes a price that is returned by default without any additional processing. This is the simplest case which, though it does nothing, is needed to make the polymorphic behavior of the execute method work for all other cases. You have undoubtedly come across situations like this before. If a parameter is not "" [Empty String] then do something. This acts the same but with a default "empty" implementation to avoid the <cfif> and make the method truly polymorphic. This is the case for a standard adult ticket.

    <cfcomponent extends="abstractStrategy"
    displayname="PricingStrategy">
    <cffunction name="Execute" access="public">
    <cfargument name="price" required="true"
    type="numeric">
    <cfreturn arguments.price>
    </cffunction>
    </cfcomponent>

    Derived from PricingStrategy.cfc is ChildPricingStrategy.cfc, which has an Execute() method that takes a price argument and returns that price multiplied by the child discount rate. For this example I have hard-coded at .5 or 50%, but it could be passed in as an argument, or the ChildPricingStrategy.cfc could call some function to query it from a database.

    <cfcomponent extends="PricingStrategy"
    displayname="ChildPricing">
    <cffunction name="Execute" access="public">
    <cfargument name="price" required="true"
    type="numeric">
    <cfreturn arguments.price * .5>
    </cffunction>
    </cfcomponent>

    In a real implementation we could have senior pricing and other pricing categories as well.

    Another type of strategy is one that hides the implementation detail of its internal data structures. This is another CFC deriving from AbstractStrategy.cfc, which adds sales tax to a price for a particular zip code. This SalesTaxStrategy.cfc calls another CFC method, which returns the sales tax rate of the passed-in zip code. It then multiplies the price by the tax rate divided by 100 plus one.

    <cfcomponent extends="abstractStrategy"
    displayname="SalesTaxStrategy">
    <cffunction name="Execute" access="public">
    <cfargument name="price" required="true"
    type="numeric">
    <cfargument name="zipCode" required="true"
    type="numeric">
    <cfinvoke component="TaxRates"
    Method="GetRateByZip"
    ReturnVariable="thisTaxRate">
    <cfreturn arguments.price * (1 +
    thisTaxRate/100)>
    </cffunction>
    </cfcomponent>

    Recall that I said earlier that the "context" in a Strategy pattern is the single interface that encapsulates interchangeable strategies. When we utilize one of our Strategy CFCs from within its context CFC, we are composing a CFC that is made up of other CFCs. We could also achieve similar results by extending the context CFC and implementing or overriding the Execute() method within it. This is less useful because we cannot develop other contexts that may contain these strategies without also creating a separate derived class for each strategy needed for each context.

    Favoring composition over inheritance makes for less code, and allows maximum code reuse and a single point for all changes to a particular strategy. The context object within which we will contain the Pricing Strategy and Tax Strategy will be Ticket.cfc, described below. Before we get into the code for the Ticket component let's look at the UML diagram shown in Figure 1.

    First we have the AbstractStrategy.cfc, which is extended by a PricingStrategy.cfc and SalesTaxStrategy.cfc. PricingStrategy.cfc itself is extended by ChildPricingStrategy.cfc and SeniorPricingStrategy.cfc. What gets tricky is that the Ticket.cfc in the diagram is shown to have two attributes. These two attributes, TaxStrategy and PricingStrategy, can be either CFC instances or simply the textual name of the type of strategy they represent. In our case they are simply the names of the appropriate TaxStrategy and PricingStrategy. Let's take a look at the code for Ticket.cfc.

    <cfcomponent displayname="Ticket">
    <!--- Instance Structure --->
    <cfset My = StructNew()>
    <cfset My.PricingStrategy = "">
    <cfset My.TaxStrategy = "">

    <cffunction name="init" access="public">
    <cfargument name="TaxStrategy"
    required="true"
    type="string"
    default="Sales">
    <cfargument name="PricingStrategy"
    required="true"
    type="string"
    default="">
    <cfset My.PricingStrategy =
    arguments.PricingStrategy>
    <cfset My.TaxStrategy =
    arguments.TaxStrategy>
    </cffunction>
    <cffunction name="getPrice" access="public">
    <cfargument name="price"
    required="true"
    type="numeric">
    <cfargument name="PricingStrategy"
    required="true" type="string"
    default="#My.PricingStrategy#">
    <cfargument name="ZipCode"
    required="true"
    type="string"
    default="19341">
    <cfset priceObj = createObject("component","com.TicketLeap.
    #arguments.Pricing
    Strategy#Pricing
    Strategy")>
    <cfset taxObj = createObject("component","com.TicketLeap.
    #my.TaxStrategy#
    TaxStrategy")>
    <cfreturn taxObj.Execute(priceObj.
    Execute(arguments.price),
    arguments.ZipCode)>
    </cffunction>
    </cfcomponent>

    In the two createObject() method calls I am using the name of the specific pricing strategy or tax strategy to dynamically create the appropriate CFC instance. I am also using a fully qualified component path since these components are most likely not in the application's directory. The string com.TicketLeap.SalesTaxStrategy will look for a CFC named SalesTaxStrategy.cfc in the /com/TicketLeap folder.

    Ticket.cfc has an Init() method, which mimics a Java constructor. It takes two strings for the TaxStrategy and default PriceStrategy that would normally be blank. It has an additional method GetPrice(), which calculates the price using the appropriate PricingStrategy and TaxStrategy CFCs. A simple implementation using these strategies is shown in Listing 1. This includes a sample query resultset of "cartitems" so you can test on your own without a database.

    First we create our Ticket CFC and call the Init() method, passing in the string "Sales" for the TaxStrategy argument. We are defaulting to undiscounted pricing so we do not need to pass in a default PricingStrategy. The string "Sales" is used to indicate we are charging sales tax on this transaction. We then have a title row before we begin to loop through the query of "cartitems". Let's look at the fields returned by this query:

  • Event: The textual name of the event the tickets are being sold for
  • Price: The standard price before any discounts
  • DiscountType: The name of an applicable discount (i.e., child, senior, etc.)
  • ZipCode: The zip code for calculating tax for tickets, if applicable

    For each row we then output the Event name, the price (dollar-formatted of course), the discount type if any, and then we calculate the actual price by passing the price, discounttype, and zipcode from the query to the Execute() method of the ticket.cfc we instantiated previously. We then output the actual price after discounts and sales tax are calculated. The output of MyCart.cfm is shown in Figure 2.

    My client was delighted with the way the cart worked and my fellow developers were delighted with the way the Pricing Strategy worked. My team and I were most happy knowing we could easily extend the system to include any new pricing class with virtually no change to existing code. Let's take a look at what we have accomplished:

  • We made the pricing system somewhat future-proof. It should work for any conceivable discount type that may be added in the future.
  • We helped ourselves with code reuse and polymorphism by segmenting out our code using a general strategy interface, which was inherited from AbstractStrategy.cfc.
  • We favored composition over inheritance in implementing our strategies.
  • We programmed to the strategy interface established in AbstractStrategy.cfc instead of to a specific implementation.

    The actual finished page from the TicketLeap.com cart is shown in Figure 3.

    Conclusion
    Overall the Strategy pattern is extremely useful in working with families of algorithms and making them interchangeable and easily extensible. As with everything there can be drawbacks to any design or implementation. Strict adherence to or overuse of the Strategy pattern (or any pattern) can cause additional communication overhead between CFCs. Strategic use of all patterns is recommended.

  • © 2008 SYS-CON Media Inc.