Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with:

OOP Data Validation Using Validation Behaviors In ColdFusion

By Ben Nadel on
Tags: ColdFusion

In my last post on object oriented data validation and error message translation, there were some really good conversations about factoring out validation logic such that different scenarios could validate the instance data in different ways. This morning, I tried to apply that methdology by factoring out my Girl.cfc and Clothing.cfc validation logic into "validation behaviors". I am not sure what the exact definition of a behavior is, but I believe it is an ecapsulated piece of swappable functionality that implements a given interface such that it can be utilitzed by another object within an algorithm. In this case, the validation interface is only that the validation behavior has a Validate() method that takes a reference to the target bean's instance variables.

To do this, I updated the Base.cfc to have a ValidationBehavior variable and a Validate() method. This way, the validation can be passed off to the validation behavior while still allowing the object to appear as if it were a "Smart" object that knows how to validate itself:

  • <cfcomponent
  • output="false"
  • hint="I am the base component and I supply base functionality for other components.">
  •  
  • <!--- Set up instance data. --->
  • <cfset VARIABLES.Instance = {} />
  •  
  • <!---
  • The validation behavior will be used to validate the
  • instance data for this object. These can be swapped
  • at run time to utilize different validation rules.
  • --->
  • <cfset VARIABLES.ValidationBehavior = "" />
  •  
  •  
  • <cffunction
  • name="Get"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I get a value from the instance object.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Property"
  • type="string"
  • required="true"
  • hint="I am the instance data property being requested."
  • />
  •  
  •  
  • <!--- Check to see if the property exists. --->
  • <cfif NOT StructKeyExists( VARIABLES.Instance, ARGUMENTS.Property )>
  •  
  • <!--- The property is not valid. --->
  • <cfthrow
  • type="Get.InvalidProperty"
  • message="The property you tried to get was not valid."
  • detail="The property you tried to get, #UCase( ARGUMENTS.Property )#, is not a valid property of this component. Valid properties are: #StructKeyList( VARIABLES.Instance )#."
  • />
  •  
  • </cfif>
  •  
  • <!--- Return property. --->
  • <cfreturn VARIABLES.Instance[ ARGUMENTS.Property ] />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Set"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I set a value in the instance object, if a key exists.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Property"
  • type="string"
  • required="true"
  • hint="I am the instance data property being set."
  • />
  •  
  • <cfargument
  • name="Value"
  • type="Any"
  • required="true"
  • hint="I am the property value being set. I return this object for method chaining."
  • />
  •  
  •  
  • <!--- Check to see if the property exists. --->
  • <cfif NOT StructKeyExists( VARIABLES.Instance, ARGUMENTS.Property )>
  •  
  • <!--- The property is not valid. --->
  • <cfthrow
  • type="Set.InvalidProperty"
  • message="The property you tried to set was not valid."
  • detail="The property you tried to set, #UCase( ARGUMENTS.Property )#, is not a valid property of this component. Valid properties are: #StructKeyList( VARIABLES.Instance )#."
  • />
  •  
  • </cfif>
  •  
  •  
  • <!--- Set property. --->
  • <cfset VARIABLES.Instance[ ARGUMENTS.Property ] = ARGUMENTS.Value />
  •  
  • <!--- Return This reference for method chaining. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="SetValidationBehavior"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I set the validation behavior for this object.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="ValidationBehavior"
  • type="any"
  • required="true"
  • hint="I am the validation behavior."
  • />
  •  
  • <!--- Store the validation behavior. --->
  • <cfset VARIABLES.ValidationBehavior = ARGUMENTS.ValidationBehavior />
  •  
  • <!--- Return This reference for method chaining. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Validate"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="I use the validation behavior to validate this object's properties.">
  •  
  • <!--- Return the validation. --->
  • <cfreturn VARIABLES.ValidationBehavior.Validate(
  • VARIABLES.Instance
  • ) />
  • </cffunction>
  •  
  • </cfcomponent>

Notice that we also have the method SetValidationBehavior(). This method allows us to determine at runtime which set of validation logic will be used to validate our objects. This logic might need to change depending on the current user or touch-point in the application and so, we can swap one behavior out for another as we see fit. The Validate() method, then passes the bean's instance data off to the Validate() method of the valdiation behavior instance.

Once I had the concept of a validation behavior, I had to start building the validation behaviors for our Girl.cfc and our Clothing.cfc. To start with, I created a BaseValidation.cfc which all the Validation behaviors would extend:

  • <cfcomponent
  • output="false"
  • hint="I provide base validation behaviors.">
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I return an initialized component.">
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Validate"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="I validate the instance data and return a struct of invalid properties.">
  •  
  • <!--- This method is meant to be overriden. --->
  • <cfthrow
  • type="Validate.MethodNotImplemented"
  • message="The Validate() method has not been implemented."
  • detail="The Validate() method is meant to be overriden in your extending class."
  • />
  • </cffunction>
  •  
  • </cfcomponent>

While I am not really taking much advantage of it at the moment, this base validation object could house utility functions that would be common to all validation behaviors.

Notice also that the BaseValidation.cfc ColdFusion component has a Validate() method that must be overriden. This method method is the full definition of the "Validation Interface." It is the now the job of the individual behaviors to implement that abstract method. In this go-round, implementation simply meant taking the validation logic out of the beans and putting it into the individual behaviors.

Here is the ClothingValidation.cfc:

  • <cfcomponent
  • extends="BaseValidation"
  • output="false"
  • hint="I provide validation for Clothing.cfc instances.">
  •  
  •  
  • <cffunction
  • name="Validate"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="I validate the instance data and return a struct of invalid properties.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Properties"
  • type="struct"
  • required="true"
  • hint="I am the instance properties for the object being validated."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {
  • Errors = {}
  • } />
  •  
  • <!---
  • Because our Setter methods are generic, we have to
  • no only check the data values but the type of data
  • as well. Nothing here can be assumed.
  • --->
  •  
  • <!--- Validate Name. --->
  • <cfif NOT IsSimpleValue(ARGUMENTS.Properties.Name )>
  •  
  • <cfset LOCAL.Errors.Name = "Name must be a string value" />
  •  
  • <cfelseif NOT Len(ARGUMENTS.Properties.Name )>
  •  
  • <cfset LOCAL.Errors.Name = "Name must be greater than zero characters" />
  •  
  • <cfelseif (Len(ARGUMENTS.Properties.Name ) GT 30)>
  •  
  • <cfset LOCAL.Errors.Name = "Name must be less than or equal to 30 characters" />
  •  
  • <cfelseif REFind( "[^\w \-']+",ARGUMENTS.Properties.Name )>
  •  
  • <cfset LOCAL.Errors.Name = "Name can only contain alpha-numeric characters and spaces" />
  •  
  • </cfif>
  •  
  •  
  • <!--- Validate Size. --->
  • <cfif NOT IsSimpleValue(ARGUMENTS.Properties.Size )>
  •  
  • <cfset LOCAL.Errors.Size = "Size must be a string value" />
  •  
  • <cfelseif NOT Len(ARGUMENTS.Properties.Size )>
  •  
  • <cfset LOCAL.Errors.Size = "Size must be greater than zero characters" />
  •  
  • <cfelseif (Len(ARGUMENTS.Properties.Size ) GT 10)>
  •  
  • <cfset LOCAL.Errors.Size = "Size must be less than or equal to 10 characters" />
  •  
  • <cfelseif REFind( "[^\w\-]+",ARGUMENTS.Properties.Size )>
  •  
  • <cfset LOCAL.Errors.Name = "Size can only contain alpha-numeric characters and dashes" />
  •  
  • </cfif>
  •  
  •  
  • <!--- Return the errors. --->
  • <cfreturn LOCAL.Errors />
  • </cffunction>
  •  
  • </cfcomponent>

... and the GirlValidation.cfc

  • <cfcomponent
  • extends="BaseValidation"
  • output="false"
  • hint="I provide validation for Girl.cfc instances.">
  •  
  •  
  • <cffunction
  • name="Validate"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="I validate the instance data and return a struct of invalid properties.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Properties"
  • type="struct"
  • required="true"
  • hint="I am the instance properties for the object being validated."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {
  • Errors = {}
  • } />
  •  
  • <!---
  • Because our Setter methods are generic, we have to
  • no only check the data values but the type of data
  • as well. Nothing here can be assumed.
  • --->
  •  
  • <!--- Validate FirstName. --->
  • <cfif NOT IsSimpleValue( ARGUMENTS.Properties.FirstName )>
  •  
  • <cfset LOCAL.Errors.FirstName = "FirstName must be a string value" />
  •  
  • <cfelseif NOT Len( ARGUMENTS.Properties.FirstName )>
  •  
  • <cfset LOCAL.Errors.FirstName = "FirstName must be greater than zero characters" />
  •  
  • <cfelseif (Len( ARGUMENTS.Properties.FirstName ) GT 30)>
  •  
  • <cfset LOCAL.Errors.FirstName = "FirstName must be less than or equal to 30 characters" />
  •  
  • <cfelseif REFind( "(?i)[^a-z \-']+", ARGUMENTS.Properties.FirstName )>
  •  
  • <cfset LOCAL.Errors.FirstName = "FirstName can only contain alpha characters, spaces, and punctuation" />
  •  
  • </cfif>
  •  
  •  
  • <!--- Validate LastName. --->
  • <cfif NOT IsSimpleValue( ARGUMENTS.Properties.LastName )>
  •  
  • <cfset LOCAL.Errors.LastName = "LastName must be a string value" />
  •  
  • <cfelseif NOT Len( ARGUMENTS.Properties.LastName )>
  •  
  • <cfset LOCAL.Errors.LastName = "LastName must be greater than zero characters" />
  •  
  • <cfelseif (Len( ARGUMENTS.Properties.LastName ) GT 50)>
  •  
  • <cfset LOCAL.Errors.LastName = "LastName must be less than or equal to 50 characters" />
  •  
  • <cfelseif REFind( "(?i)[^a-z \-']+", ARGUMENTS.Properties.LastName )>
  •  
  • <cfset LOCAL.Errors.LastName = "LastName can only contain alpha characters, spaces, and punctuation" />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Validate clothing. Since we don't know much about
  • how to validate clothing, all we can really do is
  • ask the clthing to validate itself and then add
  • it's keys to our errors message. Since we can have
  • N pieces of clothing, store the error structs at
  • index-sensitive keys.
  • --->
  • <cfloop
  • index="LOCAL.ClothesIndex"
  • from="1"
  • to="#ArrayLen( ARGUMENTS.Properties.Clothes )#"
  • step="1">
  •  
  • <!--- Get a pointer to the clothing item. --->
  • <cfset LOCAL.Clothing = ARGUMENTS.Properties.Clothes[ LOCAL.ClothesIndex ] />
  •  
  • <!---
  • Before we pass validation off to the clothing
  • object, we have to check to make sure this is
  • even a clothing object.
  • --->
  • <cfif (ListLast( GetMetaData( LOCAL.Clothing ).Name, "." ) NEQ "Clothing")>
  •  
  • <!---
  • This clothing item is not valid. Create a
  • struct with a special member to describe
  • this. This way, we keep the interface very
  • consistent - always a struct.
  • --->
  • <cfset LOCAL.Errors[ "Clothes.#LOCAL.ClothesIndex#" ] = {
  • ObjectType = "Not a valid Clothing object"
  • }/>
  •  
  • <cfelse>
  •  
  • <!---
  • This object is a valid clothes object. Now,
  • let's get the error objects from the piece
  • of clothing.
  • --->
  • <cfset LOCAL.SubErrors = LOCAL.Clothing.Validate() />
  •  
  • <!--- Check to see if any errors came back. --->
  • <cfif StructCount( LOCAL.SubErrors )>
  •  
  • <!---
  • The clothing item came back with errors,
  • so store those in an index-sensitive way
  • in the errors.
  • --->
  • <cfset LOCAL.Errors[ "Clothes.#LOCAL.ClothesIndex#" ] = LOCAL.SubErrors />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  •  
  • <!--- Return the errors. --->
  • <cfreturn LOCAL.Errors />
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, I didn't really change this logic in any way from the previous post. The only real difference is that the validation logic is working on the passed-in Properties argument rather than working directly on the instance variables. This methodology allows us to swap out behavior methods at run time without having the beans become too highly coupled to the behavior implementations.

This leaves us with fairly simple beans. Our Girl.cfc and Clothing.cfc ColdFusion components now have nothing more than their own Init() methods and default instance data values.

Here is the new Clothing.cfc:

  • <cfcomponent
  • extends="Base"
  • output="false"
  • hint="I am a piece of clothing.">
  •  
  • <!--- Set up default instance data. --->
  • <cfset VARIABLES.Instance = {
  • Name = "",
  • Size = ""
  • } />
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I initalize and pass back the object.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Name"
  • type="string"
  • required="false"
  • default=""
  • hint="I am the name of the clothing."
  • />
  •  
  • <cfargument
  • name="Size"
  • type="string"
  • required="false"
  • default=""
  • hint="I am the size of the clothing."
  • />
  •  
  • <!--- Store the instance data. --->
  • <cfset VARIABLES.Instance.Name = ARGUMENTS.Name />
  • <cfset VARIABLES.Instance.Size = ARGUMENTS.Size />
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  • </cfcomponent>

... and the new Girl.cfc:

  • <cfcomponent
  • extends="Base"
  • output="false"
  • hint="I am a girl and I like to wear clothing.">
  •  
  • <!--- Set up default instance data. --->
  • <cfset VARIABLES.Instance = {
  • FirstName = "",
  • LastName = "",
  • Clothes = []
  • } />
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I initalize and pass back the object.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="FirstName"
  • type="string"
  • required="false"
  • default=""
  • hint="I am the first name of the girl."
  • />
  •  
  • <cfargument
  • name="LastName"
  • type="string"
  • required="false"
  • default=""
  • hint="I am the last name of the girl."
  • />
  •  
  • <cfargument
  • name="Clothes"
  • type="array"
  • required="false"
  • default="#ArrayNew( 1 )#"
  • hint="I am an array of clothing objects."
  • />
  •  
  • <!--- Store the instance data. --->
  • <cfset VARIABLES.Instance.FirstName = ARGUMENTS.FirstName />
  • <cfset VARIABLES.Instance.LastName = ARGUMENTS.LastName />
  • <cfset VARIABLES.Instance.Clothes = ARGUMENTS.Clothes />
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, there's really nothing going on in the beans anymore. I was afraid that this mean that they were becoming quite anemic, but as Jeff Chastain reassures me, factoring out validation logic does not necessarily make a bean anemic (as it can have lots of other related functionality).

Ok, now onto the the action page that actually handles the form submission and the data validation. I have updated this in a few ways. For starters, I have added the validation behaviors from above. Secondly, I have moved the error flags translations out of the controller and into a separate display file that was more view specific. For some reason, I didn't like the error message translation code in the controller; it made the code look too cluttered. Moving it into a separate display file made it feel more coupled to the View (which is really what makes more sense), and makes the code seem more organized in general.

  • <!--- Param the form variables. --->
  • <cfparam name="FORM.first_name" type="string" default="" />
  • <cfparam name="FORM.last_name" type="string" default="" />
  • <cfparam name="FORM.clothes" type="regex" pattern="^$|(\d+,?)+" default="" />
  • <cfparam name="FORM.submitted" type="boolean" default="false" />
  •  
  • <!--- Param 10 pieces of clothing. --->
  • <cfloop
  • index="intClothingIndex"
  • from="1"
  • to="10"
  • step="1">
  •  
  • <cfparam
  • name="FORM.clothing_name_#intClothingIndex#"
  • type="string"
  • default=""
  • />
  •  
  • <cfparam
  • name="FORM.clothing_size_#intClothingIndex#"
  • type="string"
  • default=""
  • />
  •  
  • </cfloop>
  •  
  •  
  •  
  • <!--- Check to see if form has been submitted. --->
  • <cfif FORM.submitted>
  •  
  • <!---
  • Create the validation behavior for our girl. This is
  • an instance of an interchangable behavior that will
  • validate the girl data at runtime.
  • --->
  • <cfset objGirlValidation = CreateObject(
  • "component",
  • "GirlValidation"
  • ).Init()
  • />
  •  
  • <!---
  • Create the validation behavior for the clothes. This is
  • an instance of an interchangable behavior that will
  • validate the clothing data at runtime.
  • --->
  • <cfset objClothingValidation = CreateObject(
  • "component",
  • "ClothingValidation"
  • ).Init()
  • />
  •  
  •  
  • <!---
  • Create our girl object and set the first and last name
  • properties as well as the validation behavior.
  • --->
  • <cfset objGirl = CreateObject( "component", "Girl" )
  • .Init()
  • .SetValidationBehavior( objGirlValidation )
  • .Set( "FirstName", FORM.first_name )
  • .Set( "LastName", FORM.last_Name )
  • />
  •  
  • <!--- Create an array for the clothes. --->
  • <cfset arrClothes = [] />
  •  
  • <!---
  • Loop over the clothes in the form and create a
  • clothing object for each one.
  • --->
  • <cfloop
  • index="intIndex"
  • list="#FORM.clothes#"
  • delimiters=",">
  •  
  • <!---
  • Check to see if there is a name for the piece of
  • clothing. Since there could be N number of clothing
  • items, let's not care about the ones without names.
  • --->
  • <cfif Len( FORM[ "clothing_name_#intIndex#" ] )>
  •  
  • <!---
  • Create a clothing instance and set its properties
  • as well as it's validation behavior.
  • --->
  •  
  • <cfset ArrayAppend(
  • arrClothes,
  • CreateObject( "component", "Clothing" )
  • .Init()
  • .SetValidationBehavior( objClothingValidation )
  • .Set( "Name", FORM[ "clothing_name_#intIndex#" ] )
  • .Set( "Size", FORM[ "clothing_size_#intIndex#" ] )
  • ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  •  
  • <!---
  • Dress the girl using the array of clothing instances
  • that we created above.
  • --->
  • <cfset objGirl.Set( "Clothes", arrClothes ) />
  •  
  •  
  • <!---
  • At this point, we have a fully populated Girl object,
  • complete with clothes. Now, let's validate the girl to
  • see if the form data is valid. This will use the girl
  • validation behavior as well as the nested clothing
  • validation behaviors that we set above.
  • --->
  • <cfset REQUEST.DataErrors = objGirl.Validate() />
  •  
  • <!---
  • Check to see if there are any data errors. If ther are
  • no errors, then forward to the confirmation page. If
  • there are errors, then just let the form display again.
  • --->
  • <cfif NOT StructCount( REQUEST.DataErrors )>
  •  
  • <!---
  • Save the user to database. Since this is just a
  • demo on data validation, I am not demonstrating
  • this. Might something like:
  •  
  • GirlService.Save( objGirl );
  • --->
  •  
  • <!--- Relocate to confirmation. --->
  • <cflocation
  • url="#CGI.script_name#?action=confirm"
  • addtoken="false"
  • />
  •  
  •  
  • </cfif>
  •  
  • </cfif>

Notice that as we are processing the form data, we create an instance of the GirlValidation.cfc as well as the ClothingValidation.cfc and inject those into the appropriate instances before we Validate() the data. As a side effect of this process, we do get something nice to happen - we use one instance of the ClothingValidation.cfc ColdFusion component to validate all of our Clothing.cfc instances. Since the validation logic is not stateful and depends only on the data that is passed to it, we have now created one instance of the validation code, rather than N instance for N pieces of clothing. Plus, this means that we can cache singletons of our validation logic in the APPLICATION scope, or even the SESSION scope, and then use those at runtime to do the validation. Something about that is deeply satisfying.

The Validate() method returns a property-indexed struct just as it did in the previous blog post. This time, however, rather than handling that logic in the controller, I have moved the error translation into a separate file, dsp_form_display.cfm, that deals just with error messages for just this view:

  • <!--- Check to see if we have any data errors. --->
  • <cfif StructCount( REQUEST.DataErrors )>
  •  
  • <!---
  • Create an errors array. This will hold the error
  • messages that get displayed on the page.
  • --->
  • <cfset arrErrors = [] />
  •  
  •  
  • <!--- Map the data errors to the array. --->
  • <cfif StructKeyExists( REQUEST.DataErrors, "FirstName" )>
  •  
  • <!--- Add the error message. --->
  • <cfset ArrayAppend(
  • arrErrors,
  • "Please enter a valid first name."
  • ) />
  •  
  • </cfif>
  •  
  • <cfif StructKeyExists( REQUEST.DataErrors, "LastName" )>
  •  
  • <!--- Add the error message. --->
  • <cfset ArrayAppend(
  • arrErrors,
  • "Please enter a valid last name."
  • ) />
  •  
  • </cfif>
  •  
  • <!---
  • Loop over the clothing index to see if any
  • of them errored.
  • --->
  • <cfloop
  • index="intClothingIndex"
  • from="1"
  • to="10"
  • step="1">
  •  
  • <cfif StructKeyExists( REQUEST.DataErrors, "Clothes.#intClothingIndex#" )>
  •  
  • <!---
  • At this point, we know that something in the
  • clothing object errored. Now, we have to
  • figure out what that was.
  • --->
  • <cfif StructKeyExists( REQUEST.DataErrors[ "Clothes.#intClothingIndex#" ], "Name" )>
  •  
  • <!--- Add the error message. --->
  • <cfset ArrayAppend(
  • arrErrors,
  • "Clothing item ###intClothingIndex# has an invalid name"
  • ) />
  •  
  • </cfif>
  •  
  • <cfif StructKeyExists( REQUEST.DataErrors[ "Clothes.#intClothingIndex#" ], "Size" )>
  •  
  • <!--- Add the error message. --->
  • <cfset ArrayAppend(
  • arrErrors,
  • "Clothing item ###intClothingIndex# has an invalid size"
  • ) />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  •  
  • </cfloop>
  •  
  •  
  •  
  • <!--- Check to see if there are any form errors. --->
  • <cfif ArrayLen( arrErrors )>
  •  
  • <cfoutput>
  •  
  • <p>
  • <strong>Please review the follwoing:</strong>
  • </p>
  •  
  • <ul>
  • <cfloop
  • index="strError"
  • array="#arrErrors#">
  •  
  • <li>
  • #strError#
  • </li>
  •  
  • </cfloop>
  • </ul>
  •  
  • </cfoutput>
  •  
  • </cfif>
  •  
  • </cfif>

This file is then included more cleanly in the actual form dispaly page, dsp_form.cfm:

  • <cfoutput>
  •  
  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>OOP Data Validation Demo</title>
  • </head>
  • <body>
  •  
  • <h1>
  • OOP Validation Demo - Enter Girl Data
  • </h1>
  •  
  •  
  • <!--- Include the form errors. --->
  • <cfinclude template="dsp_form_errors.cfm" />
  •  
  •  
  • <form action="#CGI.script_name#" method="post">
  •  
  • <!--- Submission flag. --->
  • <input
  • type="hidden"
  • name="submitted"
  • value="1"
  • />
  •  
  • <!--- Pass back action. --->
  • <input
  • type="hidden"
  • name="action"
  • value="#REQUEST.Attributes.action#"
  • />
  •  
  •  
  • <label>
  • First Name:<br />
  • <input
  • type="text"
  • name="first_name"
  • value="#FORM.first_Name#"
  • size="40"
  • />
  • </label>
  • <br />
  • <br />
  •  
  • <label>
  • Last Name:<br />
  • <input
  • type="text"
  • name="last_name"
  • value="#FORM.last_Name#"
  • size="40"
  • />
  • </label>
  • <br />
  •  
  •  
  • <h4>
  • Clothes
  • </h4>
  •  
  • <!---
  • Let's allow for up to 5 peices of clothing.
  • Loop over an create the form fields.
  • --->
  • <cfloop
  • index="intClothingIndex"
  • from="1"
  • to="5"
  • step="1">
  •  
  • <!---
  • This will become the comma-delimited list
  • of our clothing items.
  • --->
  • <input
  • type="hidden"
  • name="clothes"
  • value="#intClothingIndex#"
  • />
  •  
  • <label>
  • Name:
  • <input
  • type="text"
  • name="clothing_name_#intClothingIndex#"
  • value="#FORM[ "clothing_name_#intClothingIndex#" ]#"
  • size="40"
  • />
  • </label>
  • <br />
  •  
  • <label>
  • Size:
  • <input
  • type="text"
  • name="clothing_size_#intClothingIndex#"
  • value="#FORM[ "clothing_size_#intClothingIndex#" ]#"
  • size="20"
  • />
  • </label>
  • <br />
  • <br />
  •  
  • </cfloop>
  •  
  •  
  • <input type="submit" value="Save Girl" />
  •  
  • </form>
  •  
  •  
  • <!---
  • For debugging, let's output data validation errors
  • if they exist.
  • --->
  • <cfif StructKeyExists( REQUEST, "DataErrors" )>
  •  
  • <h2>
  • Data Validation Errors
  • </h2>
  •  
  • <cfdump
  • var="#REQUEST.DataErrors#"
  • label="Data validation errors"
  • />
  •  
  • </cfif>
  •  
  • </body>
  • </html>
  •  
  • </cfoutput>

So that's my attempt at factoring out the validation logic into a behavior-based system. Despite the fact that we have a seemingly large number of files to handle such a small piece of functionality, there is something very appealing about it. You still get to leverage the "Smart Object" scenario where the Model knows how to validate itself, but at the same time, you also get the benefit of being able to swap out the validation logic if needed. This really leaves your code "open" for extension and "closed" for change, which, if you have read any design pattern books, is something we are definitely shooting for.

I also liked moving the error flag to message translation in to a sepparate display file. This created a nice, encapsulated piece of functionality; and, it didn't feel like it was stepping on anyone else's toes.

What I am curious about, though, is what happens when the Validate() method needs data outside of itself? What happens when the validation is a business rule that involves other parts of the system. For example, what if the validation needed to make sure that no other Girls in the system existed with the same first and last name combination. At that point, we need some sort of a Girl Service. But where does that get passed in? I assume not as part of the Validate() method as its interface only expects the Instance variables. Then, perhaps, other helper methods such as SetGirlService() which will set an internal variable to be used during validation? More to come on that I suppose.

But, is this starting to get a bit overkill for such a simple form validation? At face value, of course. But, again, we have to remember that the benefit of object oriented programming is not so much in the one-off, single tasks; it's in the long term maintenance and scalability of our applications. So, in that respect, we have something cool happening here.




Reader Comments

Ben, I'd recommend using a Factory to create the correct Validation object rather than manually having to create it and pass it in. That would get very difficult to maintain.

Regarding cases where the Validator needs information outside of itself, this can be handled using a Custom Validator and managing it with ColdSpring. For example, your generic Validator may handle general things like a field being required (not empty), a field being less than or greater than a certain number of characters, etc. But you could also have custom validators for cases where more data is needed. Say you want to check user name uniqueness. You may have a UserValidator that extends BaseValidator and can handle this check. You'd have ColdSpring inject a reference to a UserService, and the UserValidator could call userService.isUserNameUnique(userName).

A nice side-effect of this is that if you implemented some sort of JavaScript/AJAX validation in the HTML form, it could make a call to the same service method to alert the user right in the form that the user name was already in use. Hopefully this makes sense.

Regards,

Brian

@Brian,

That stuff makes sense, but I want to keep it as simple as possible for as long as possible. I want to understand the concepts and the problems before I even think about learning something additional like ColdSpring to help tie it all together. From everything I've heard and read, ColdSpring and IoC frameworks makes life much easier, but I have to learn to walk before I can learn to run :)

I like what you're saying about the user service and how that can be used by various forms of validation. I sort of understand the concept of dependency injection, but I don't know if I understand the practical implementation. Do you need to have a Setter / Getter for each dependency that you want to inject?

For example, using your comment, would my validation behavior have methods for:

GetUserService()
SetUserService( objService )

"Do you need to have a Setter / Getter for each dependency that you want to inject?"
Writing a setter for each dep. is one way ColdSpring can work and it works well with 'auto wire' where CS will look at the set'ers for a CFC, and call them automatically with the correct CFC.
You can also use 'constructor' injection, where all the deps are passed into an 'init' method, but I prefer auto wire as it keeps the XML size down and means the CFC is the documentation, not the XML file.

That is all true, but the (arguably) biggest reason to favor setter injection over constructor injection is that it allows ColdSpring to easily resolve circular dependencies (where CFC A depends on CFC B, but CFC B depends on CFC A).

Yes Ben, that's exactly right. You'd have a setter for any properties that you want ColdSpring to inject for you. So if your UserValidator CFC has a setUserService method (with userService as an argument), ColdSpring will look for a matching UserService bean that it is managing and inject it using that setter. This is "autowiring by name". For more clarity, you can explicitly declare the dependency which is generally preferred. In the XML you'd just have:

<bean name="userService" class="app.cfcs.UserService">
<bean name="userValidator" class="app.cfcs.UserValidator">
<property name="userService">
<ref bean="userService" />
</property>
</bean>