The Strategy Design Pattern in ColdFusion

Thoughts on the Strategy design pattern specific to ColdFusion and it's typeless nature.

What is the Strategy design pattern?

Suppose you have an object A which needs to use another object B.

[A] --- uses ---> [B]

Then a different situation requires that you need to exchange B for another object C; so object A is then making use of C.

[A] --- uses ---> [C]

The ability to exchange objects B and C without changing object A is the Strategy design pattern in action.

In order for this to be possible, objects B and C need to both implement the common set of functions that object A requires.

In this description, the object A is called our context. The objects B anc C are our strategy objects.

What problem does the Strategy pattern solve?

The Strategy pattern makes it easy to change the behaviour of an algorithm used by the context component. As long as any new strategy component includes the common set of functions required, it may be used within the context component.

Example: Form field validation

Suppose that you have a form that has three text fields. One of the fields is a name that may be up to 50 characters, then second field is an email address, and the third field is a number. We need to provide code to validate these fields.

We can consider the following components may be used here:

  1. TextField - holds the data entered by the user
  2. LengthValidator - knows how to check the length of a string
  3. EmailValidator - knows how to check if a string is an email address
  4. NumberValidator - knows how to check if a string is numeric

In this example, the TextField is our context object. The other three are our strategy objects. In order for our strategy objects to be useful, they must all share a common set of functions. In this case they all share one function:

<!--- Validates the string and returns a true if it satisfies the validation criteria --->
<cffunction name="validate" returntype="boolean" output="false">
   <cfargument name="text" type="string" required="true">
   <!--- Implement function here --->
</cffunction>

Let's look at each of our objects in more detail:

TextField
init(text) - Sets the initial text in the field.
addValidator(validator) - Adds a validator that must be applied to the field.
isValid() - Returns true if all of the added validators return true.

LengthValidator
init(minLength,maxLength) - Initialiser to specify the min and max lengths permitted.
validate(text) - Returns true if the length of the text is within the initialiser range.

EmailValidator
init() - Initialiser does nothing.
validate(text) - Returns true if the provided text is an email address.

NumberValidator
init() - Initialiser does nothing.
validate(text) - Returns true if the provided text is an a number.

In this example, the strategy object is added to the context via an 'addValidator' function. If the context object only requires a single strategy it could be supplied via the init() function.

Now some code to see how the TextField might be coded

<!--- TextField.cfc --->
<cfcomponent output="false">

   <cfset variables.text = "">
   <cfset variables.validators = arrayNew(1)>

   <!--- Initialise our text field with some text --->
   <cffunction name="init" output="false">
      <cfargument name="text" type="string" required="true">
      <cfset variables.text = arguments.text>
      <cfreturn this>
   </cffunction>

   <!--- Add any kind of validator to our field --->
   <cffunction name="addValidator" output="false">
      <!--- Notice type="any" here to allow any kind of validator --->
      <cfargument name="validator" type="any" required="true">
      <cfset arrayAppend(variables.validators, arguments.validator)>
   </cffunction>

   <!--- Loop through each validator and apply to the stored text data --->
   <cffunction name="isValid" output="false">
      <cfset var i = "">
      <cfset validator = "">
      <cfloop index="i" from="1" to="#arrayLen(variables.validators)#">
         <cfset validator = variables.validators[i]>
         <!--- We require that the validator has one function called 'validate'
         that knows how to validate a text string --->

         <cfif not validator.validate(variables.text)>
            <cfreturn false>
         </cfif>
      </cfloop>
      <cfreturn true>
   </cffunction>

</cfcomponent>

Let's look at an example of how one of the validators might be implemented:

<!--- LengthValidator.cfc --->
<cfcomponent output="false">

   <cfset variables.min = "">
   <cfset variables.max = "">

   <!--- Initialise our min and max values --->
   <cffunction name="init" output="false">
      <cfargument name="min" type="numeric" required="true">
      <cfargument name="max" type="numeric" required="true">
      <cfset variables.min = arguments.min>
      <cfset variables.max = arguments.max>
      <cfreturn this>
   </cffunction>

   <!--- Return true if the length of the supplied text is within the range --->
   <cffunction name="validate" output="false" returntype="boolean">
      <cfargument name="text" type="string" required="true">
      <cfif len(arguments.text) ge variables.min
         and len(arguments.text) le variables.max>

         <cfreturn true>
      </cfif>
      <cfreturn false>
   </cffunction>

</cfcomponent>

Now let's see how our components might be used together:

<!--- Create some validators --->
<cfset lengthValidator = createObject("component","LengthValidator").init(1,50)>
<cfset emailValidator = createObject("component","EmailValidator").init()>
<cfset numberValidator = createObject("component","NumberValidator").init()>
<!--- Create our text fields. We hard code the initial text for now,
but in a real application this data would come from a form submitted by the user --->

<cfset nameField = createObject("component","TextField").init("Jim Boss")>
<cfset emailField = createObject("component","TextField").init("jimboss.com")>
<cfset numField = createObject("component","TextField").init("99")>
<!--- Now add the validators to the fields --->
<cfset nameField.addValidator(lengthValidator)>
<cfset emailField.addValidator(emailValidator)>
<cfset numField.addValidator(numberValidator)>
<!--- Now check if our fields are valid --->
<cfoutput>
#nameField.isValid()#<br />
#emailField.isValid()#<br />
#numField.isValid()#<br />
</cfoutput>

This would display the output

true
false
true

Example: Sorting Objects

This example demonstrates separating out a sorting algorithm from a collection of objects.

<!--- Assume we have some kind of collection object that allows person names to be added. --->
<cfset names = createObject("component","NameCollection").init()>

<!--- Add some strings to the collection --->
<cfset names.add("Tim","Burton")>
<cfset names.add("Peter","Jackson")>
<cfset names.add("David","Lynch")>
<cfset names.add("David","Cronenberg")>

<!--- Sort the collection by first name --->
<cfset sortStrategy = createObject("component","FirstNameSort").init()>
<cfset collection.setSortStrategy(sortStrategy)>
<cfset collection.sort()>

<!--- Sort the collection by last name --->
<cfset sortStrategy = createObject("component","LastNameSort").init()>
<cfset collection.setSortStrategy(sortStrategy)>
<cfset collection.sort()>

In this example, the NameCollection is our context component, and the FirstNameSort and LastNameSort are our strategy components.

How to identify the Strategy design pattern

Couple of notes about how to identifiy the Strategy design pattern:

  • There must be a context object
  • There must be one or more strategy objects that have a shared set of common functions
  • The strategy objects are stored inside the context object and are supplied either via the init() function or via a setStrategy() type function.

The Strategy pattern in ColdFusion vs other languages

Descriptions of the Strategy pattern in some other languages (such as Java) will usually refer to interfaces. An interface is used to define the common set of functions that all of the strategy objects use. Due to ColdFusion's typeless nature and runtime checking of types, interfaces are not very relevant in the language so I have not included them here.

Comments
26 Feb 2008 03:23PM
berto said:
I understand that you are just showing an example of this design pattern in use, but this is just way too much code for a simple validation
28 Feb 2008 12:07PM
Hi berto

Yes, for typical day to day work with hand coded forms all of this would be overkill. The original idea behind the example was around developing a system that allows forms to be created by end users (via a form building user interface) then this technique would become more useful. I notice I didn't mention that!

Would be good to come up with a more common practical example.
Add a comment
(will not be published)
(include http://)