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 cf.Objective() 2012 (Minneapolis, MN) with:

Object Oriented Data Validation And Error Message Translation

By Ben Nadel on
Tags: ColdFusion

Yesterday, I discussed that as I am learning more and more about Object Oriented Programming (OOP) in ColdFusion, one of the biggest mental hurdles seems to be data validation. Well, data validation and the translation of data errors into user-friendly error messages. I talked about how the translation cannot be made in the Model because that would couple the Model too tightly to the rest of the application when, in fact, the Model is not supposed to have to worry about much more than itself.

This morning, to explore this concept a bit more, I wrote a little demo code that takes a very small form page and two small objects and does all the OOP data validation goodness. The scenario is that we have this Girl.cfc object. She has a FirstName, LastName, and Clothes property. The Clothes property is an array that holds zero or more instances of the Clothing.cfc object (don't worry, on this blog it will always have at least one clothes instance). Both of these objects have a Validate() method that knows how to validate itself and its composed objects.

Now, before I get into it, I thought I would try something a little different. I recording a quick video to demo the code I created. I thought this was good because you really get to see the error object and how it is created in conjunction with the form:


 
 
 

 
 
 
 
 

Ok, so let's get down to the nitty griddy. The two Models I have, Girl.cfc and Clothing.cfc, are quite simple. They both extends a Base.cfc that has the generic Getter / Setter functionality and then implement their own Validate() method. Let's take a look at the Base.cfc:

  • <cfcomponent
  • output="false"
  • hint="I am the base component and I supply base functionality for other components.">
  •  
  • <!--- Set up instance data. --->
  • <cfset VARIABLES.Instance = {} />
  •  
  •  
  • <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>
  •  
  • </cfcomponent>

Not much going on here. The generic Get() and Set() methods can take any kind of data. The only validation that actually occurs at this level is that you can only get or set a property that is explicitly defaulted in the VARIABLES.Instance scope. Nothing too exciting.

Next, let's take a look at the Clothing.cfc:

  • <cfcomponent
  • extends="Base"
  • output="false"
  • hint="I am a piece of clothing.">
  •  
  • <!--- Set up 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>
  •  
  •  
  • <cffunction
  • name="Validate"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="I validate the data in this instance and return a struct of keys and reasons.">
  •  
  • <!--- 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( VARIABLES.Instance.Name )>
  •  
  • <cfset LOCAL.Errors.Name = "Name must be a string value" />
  •  
  • <cfelseif NOT Len( VARIABLES.Instance.Name )>
  •  
  • <cfset LOCAL.Errors.Name = "Name must be greater than zero characters" />
  •  
  • <cfelseif (Len( VARIABLES.Instance.Name ) GT 30)>
  •  
  • <cfset LOCAL.Errors.Name = "Name must be less than or equal to 30 characters" />
  •  
  • <cfelseif REFind( "[^\w \-']+", VARIABLES.Instance.Name )>
  •  
  • <cfset LOCAL.Errors.Name = "Name can only contain alpha-numeric characters and spaces" />
  •  
  • </cfif>
  •  
  •  
  • <!--- Validate Size. --->
  • <cfif NOT IsSimpleValue( VARIABLES.Instance.Size )>
  •  
  • <cfset LOCAL.Errors.Size = "Size must be a string value" />
  •  
  • <cfelseif NOT Len( VARIABLES.Instance.Size )>
  •  
  • <cfset LOCAL.Errors.Size = "Size must be greater than zero characters" />
  •  
  • <cfelseif (Len( VARIABLES.Instance.Size ) GT 10)>
  •  
  • <cfset LOCAL.Errors.Size = "Size must be less than or equal to 10 characters" />
  •  
  • <cfelseif REFind( "[^\w\-]+", VARIABLES.Instance.Size )>
  •  
  • <cfset LOCAL.Errors.Name = "Size can only contain alpha-numeric characters and dashes" />
  •  
  • </cfif>
  •  
  •  
  • <!--- Return the errors. --->
  • <cfreturn LOCAL.Errors />
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, the Clothing.cfc Model only had two properties: Name and Size. The majority of the ColdFusion component is concerned with validating the data, which is not exactly the smallest amount of code. Since our generic getter / setter methods don't provide any low level type-validation, we have to check not only the property values but also that they are the correct type of data as well.

As the Clothing.cfc runs through its data validation rules, it stores the invalid properties by key in a ColdFusion struct. Each validation error gets some sort of error explanation in the struct, but this is NOT the error message that we want to show the end user. The point here is that we are creating an "index" of invalid properties. If there are no data validation errors in this ColdFusion component, then we simply return an empty struct.

Now, let's take a look at the Girl.cfc. This is slightly more complicated because the Girl.cfc has simple properties as well as an array of composed Clothing.cfc instances:

  • <cfcomponent
  • extends="Base"
  • output="false"
  • hint="I am a girl and I like to wear clothing.">
  •  
  • <!--- Set up 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>
  •  
  •  
  • <cffunction
  • name="Validate"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="I validate the data in this instance and return a struct of keys and reasons.">
  •  
  • <!--- 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( VARIABLES.Instance.FirstName )>
  •  
  • <cfset LOCAL.Errors.FirstName = "FirstName must be a string value" />
  •  
  • <cfelseif NOT Len( VARIABLES.Instance.FirstName )>
  •  
  • <cfset LOCAL.Errors.FirstName = "FirstName must be greater than zero characters" />
  •  
  • <cfelseif (Len( VARIABLES.Instance.FirstName ) GT 30)>
  •  
  • <cfset LOCAL.Errors.FirstName = "FirstName must be less than or equal to 30 characters" />
  •  
  • <cfelseif REFind( "(?i)[^a-z \-']+", VARIABLES.Instance.FirstName )>
  •  
  • <cfset LOCAL.Errors.FirstName = "FirstName can only contain alpha characters, spaces, and punctuation" />
  •  
  • </cfif>
  •  
  •  
  • <!--- Validate LastName. --->
  • <cfif NOT IsSimpleValue( VARIABLES.Instance.LastName )>
  •  
  • <cfset LOCAL.Errors.LastName = "LastName must be a string value" />
  •  
  • <cfelseif NOT Len( VARIABLES.Instance.LastName )>
  •  
  • <cfset LOCAL.Errors.LastName = "LastName must be greater than zero characters" />
  •  
  • <cfelseif (Len( VARIABLES.Instance.LastName ) GT 50)>
  •  
  • <cfset LOCAL.Errors.LastName = "LastName must be less than or equal to 50 characters" />
  •  
  • <cfelseif REFind( "(?i)[^a-z \-']+", VARIABLES.Instance.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( VARIABLES.Instance.Clothes )#"
  • step="1">
  •  
  • <!--- Get a pointer to the clothing item. --->
  • <cfset LOCAL.Clothing = VARIABLES.Instance.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>

Again, not a whole lot going on here, but the composed objects do throw a bit of twist into the validation. Now, not only does the Girl.cfc have to validate its own simple properties, FirstName and LastName, it has to, in-turn, check the validation of each of the Clothing.cfc instances. And, since each of the Clothing.cfc instances may return like-named-errors, we have to come up with a way to create contextual errors. To do this, I have decided that I would suffix the "Clothes" property with the index of the instance the Girl.cfc Model is currently validating.

This nested data validation-flag struct ends up giving us a ColdFusion structure that could look like this:


 
 
 

 
Object Oriented Programming Data Validation Scheme With Nested Domain Models  
 
 
 

By doing this, we are creating an index of data validation errors that has nothing to do with interfaces in which they are used. The only thing that is required at this point is that the interface or the controller knows that these structs are indexed by properties and sub-context property keys.

So, that's all I have as far as the Domain Model goes - time to dive into the Controller and View pages that created the demo video you watched above. I am trying to keep this as simple as possible such that I can understand the situation more clearly without having to worry about the rest of the framework. Therefore, I am dealing with an extremely small Front Controller that handles only this form:

  • <!---
  • Create an attributes object which will combine the URL
  • and FORM scopes when the originating point of a user-
  • supplied variable is not known.
  • --->
  • <cfset REQUEST.Attributes = Duplicate( URL ) />
  • <cfset StructAppend( REQUEST.Attributes, FORM ) />
  •  
  •  
  • <!--- Param the action variable. --->
  • <cfparam
  • name="REQUEST.Attributes.Action"
  • type="string"
  • default="form"
  • />
  •  
  •  
  • <!---
  • NOTE: Normally, the code above would not be part of
  • this "controller". This would have been set by the
  • framework, but for simplicities sake, we are only using
  • a single page controller. Below this, assume that this
  • is the sub-controller for just this form.
  • --->
  •  
  •  
  • <!--- Check to see which action we are performing. --->
  • <cfswitch expression="#REQUEST.Attributes.Action#">
  •  
  • <cfcase value="confirm">
  • <cfinclude template="dsp_confirm.cfm" />
  • </cfcase>
  •  
  • <cfdefaultcase>
  • <cfinclude template="act_form.cfm" />
  • <cfinclude template="dsp_form.cfm" />
  • </cfdefaultcase>
  •  
  • </cfswitch>

Almost nothing going on here - we are either displaying the confirmation page or the form processing page.

Now, the form processing page, act_form.cfm, is where the magic is all coming together. This controller, or action file - to be honest, I am not really sure what to call this - has several steps. Let me briefly outline it:

  1. Param form data.
  2. Populate the models with form data.
  3. Call validate on model.
  4. Translate error index to user-oriented messages.

And, I'm not even taking into account the form population that would need to take place if this was an existing Girl.cfc instance - we are assuming an insert-only situation (Girl objects, insert-only situations... this sounds kind of dirty for a post about OOP, my apologies). This code is a bit complicated:

  • <!--- 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>
  •  
  •  
  • <!---
  • Create an errors array. This will hold the error messages
  • that get displayed on the page. I have chosen to put it
  • here rather than in the view because this controller is
  • meant for the view.
  • --->
  • <cfset REQUEST.Errors = [] />
  •  
  •  
  •  
  • <!--- Check to see if form has been submitted. --->
  • <cfif FORM.submitted>
  •  
  • <!--- Create our girl object with first/last name. --->
  • <cfset objGirl = CreateObject( "component", "Girl" ).Init(
  • FirstName = FORM.first_name,
  • 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#" ] )>
  •  
  • <cfset ArrayAppend(
  • arrClothes,
  • CreateObject( "component", "Clothing" )
  • .Init()
  • .Set( "Name", FORM[ "clothing_name_#intIndex#" ] )
  • .Set( "Size", FORM[ "clothing_size_#intIndex#" ] )
  • ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  •  
  • <!--- Dress the girl. --->
  • <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.
  • --->
  • <cfset REQUEST.DataErrors = objGirl.Validate() />
  •  
  • <!---
  • If there are none, then we want to comit the girl object
  • to the database and forward user to confirmation page.
  • --->
  • <cfif StructCount( REQUEST.DataErrors )>
  •  
  • <!--- Map the data errors to the array. --->
  • <cfif StructKeyExists( REQUEST.DataErrors, "FirstName" )>
  •  
  • <!--- Add the error message. --->
  • <cfset ArrayAppend(
  • REQUEST.Errors,
  • "Please enter a valid first name."
  • ) />
  •  
  • </cfif>
  •  
  • <cfif StructKeyExists( REQUEST.DataErrors, "LastName" )>
  •  
  • <!--- Add the error message. --->
  • <cfset ArrayAppend(
  • REQUEST.Errors,
  • "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(
  • REQUEST.Errors,
  • "Clothing item ###intClothingIndex# has an invalid name"
  • ) />
  •  
  • </cfif>
  •  
  • <cfif StructKeyExists( REQUEST.DataErrors[ "Clothes.#intClothingIndex#" ], "Size" )>
  •  
  • <!--- Add the error message. --->
  • <cfset ArrayAppend(
  • REQUEST.Errors,
  • "Clothing item ###intClothingIndex# has an invalid size"
  • ) />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  •  
  • </cfloop>
  •  
  •  
  • <cfelse>
  •  
  •  
  • <!---
  • 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>

The part that is tripping me up is that so much code goes into error validation. I used to feel that when it was all inline, it was still verbose, but seemed much less than this. I feel that with the error message translation, I am almost doing data validation twice, in a way. But, I have to always be keeping the mindset that this is for the long-term benefits of easy maintenance as well as unit testing (when I start doing that); I am front-loading the effort so that changes later can be done much faster and with greater confidence.

Once the action page has finished, it's pretty straight forward from there. The form display page that gets included simply outputs the form fields as well as any error messages that have been passed on from the action page:

  • <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>
  •  
  •  
  • <!--- Check to see if there are any form errors. --->
  • <cfif ArrayLen( REQUEST.Errors )>
  •  
  • <p>
  • <strong>Please review the follwoing:</strong>
  • </p>
  •  
  • <ul>
  • <cfloop
  • index="strError"
  • array="#REQUEST.Errors#">
  •  
  • <li>
  • #strError#
  • </li>
  •  
  • </cfloop>
  • </ul>
  •  
  • </cfif>
  •  
  •  
  • <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 pretty much all there is to it. I am not gonna bother showing you the code for the Confirmation page as it just outputs a confirmation message - no logic or ColdFusion code.

Time for reflection - how do I feel about all of this?? Well, for starters, I think this is the happiest I have been so far with an attempt at Model-driven data validation. I like that the Model objects are smart and know how to validate themselves. And, I really like the "indexed" error struct with nesting options that I came up with; I feel like that's a really clean way to handle the nested validation contexts that arise with composed model objects.

As far as the error message translation? I am not sure how I feel about it. I might want to move that into the View. I might want to move that into a "helper" object that some people discussed in my previous post. I think it might be a little too much for me to think about at this second (plus I have to get back to work); I am gonna let it sink in before I really make a decision.

I would love to get some feedback on this methodology! I am trying so hard to wrap my brain around all this OOP.

Also, what do you guys think of the video? Does it help explain the concept? I am thinking of trying to incorporate video a bit more to help tie it all together.




Reader Comments

This was great! The screencast really helps me understand what is going on as you move the mouse pointer over the sections! I wish I could have learned more of my CF skills this way. Nice job.

Reply to this Comment

Whenever you talk about validation in an object oriented world, the question comes up as to whether you should be stuffing potentially invalid data into a bean and then validating it, or validating the data, then stuffing it into the bean. It is a small bit of overhead, but if you validate the data first and determine it is bad, then you don't have to create the bean object.

Either way, a while back when facing this problem, I got tired of writing the same set of validation code over and over .... checking email addresses, valid date ranges, etc. So I created Validat (http://www.alagad.com/go/products-and-projects/validat-data-validation-engine/validat-data-validation-engine), a data validation engine which will take a structure of data, a business object, or virtually any other collection of data and validate that collection of data against a set of rules you have predefined. The result is an error collection which can contain resource bundle keys for the error messages, thus allowing you to customize and set those messages outside of your model.

This is not meant to be an advertisement, just an FYI if you had not heard of Validat to know that a potential solution was available. That and to raise the same question again of validation before the bean is created and stuffed or afterwards. My current preference is before as that keeps the bean simple, but I have gone back and forth.

Reply to this Comment

@Jeff,

No problem on the plug - I am all for anything that helps me to learn this stuff. Are there any articles or blog posts on how to use it. I think I understand what it is trying to do, but I am not sure about the implementation.

The other day, Peter Bell and I were talking about creating some sort of validation object - actually he mentioned that in response to all the questions I was asking him about data validation. We had thought about maybe doing this as part of the Pair Programming demo, but we didn't have nearly enough time.

I can definitely see how so much of is repetative. In fact, I could see how 90% of the validation out there could probably be done using regular expressions:

\w{1,50}

Required field between 1 and 50 characters containing "word" data. That's like the majority of stuff out there. Of course, more complex validation, like database lookups, that would have to be taken care of in other ways. Although, from your description of Validat (on the Alagad site), it looked like you could write custom validation rules that could do some stuff like that.

Can you point me to a good example? Thanks.

Reply to this Comment

@Ben,

There are a series of blog postings that were done on the Alagad blog describing the functionality and capabilities of Validat (http://www.alagad.com/go/blog?categoryId=8F6F9CEA-3048-55C9-439427FF4AD5446E). There is also some basic sample code included in the download.

As with most open source projects, it suffers from a good complete set of documentation or examples. If you have any questions, let me know. I would love to hear your feedback.

Reply to this Comment

The video was great. Now the next thing to consider is what happens when you have a change in requirements. Another form somewhere else needs to validate the data in a different way. say for different user roles or permissions, some other business rule, or return a different error message. The OO way is to encapsulate that change somehow so you are not modifying that validate function to fit both the first case and the new case every time a change occurs.

CoolJJ

Reply to this Comment

I generate XML validation files based on database metadata that I can then go in and modify as necessary. So I avoid the work of having to "translate" an error by simply returning a displayable error message as part of the Result object that is returned from the Model. Something like:

<rules custom="false">
<properties>
<property name="age">
<rule message="Age is required." required="true" />
<rule message="You must enter a valid number." type="numeric" />
<rule maxlength="3" message="Your Age cannot be more than 3 characters long." />
</property>
</properties>
</rules>

A validator factory gets the correct XML, parses it, and caches the rules for future use. It will allow for any validation types that IsValid can handle, such as SSN, zip code, etc. I can specify custom="true" and create a custom Validator for that object which will automatically be called and where I can add custom validation that is beyond the scope of the built-in validation. I've found it to be pretty flexible.

Reply to this Comment

@CoolJJ

This is where the concept of having a seperate validation engine really starts to look good. With the validation engine, you can have multiple sets of rules for data based upon a given state or any other reason. Then, you can pass the bean or the collection of data to the validation engine, letting it know which set of rules to validate the data against. This way, by keeping the actual validation logic outside of the bean, you keep it encapsulated.

Reply to this Comment

It all looks pretty good, and the implementation of a validation system is honestly the best way to approach it. The only thing I struggle with personally is tieing my server side validation engine and my client side engines together.

I personally am a big fan of not letting data go across the wire until it has been validated as much as possible on the client side. I then validate server side to make double sure.

This allows two things:
1. No needless transmission across the wire from your primary interface until client validated.
2. The server side acts as a double check, or if you are calling these server side methods from something other than your primary interface (i.e. webservice) you have the proper validation still in check.

Reply to this Comment

I also like having the validation engine separate from the object. For re-use as stated above and with reuse is less memory usage since its not in every bean. I do usually create a validation cfc for each bean and abstract the common utilities away in case I swap the common validation out. Plus this way I could pass a bean (yes I load the bean with bad data also) to the [Bean name]Validation.cfc and then it passes simple values to the common validation engine.

Reply to this Comment

@RyamTJ: Validat works exactly the same way ... it is really synonymous to your individual validation beans except with Validat you can setup different validation configurations for different states that a bean might be in. Then, when it comes time to validate data, you can pass either the dirty bean or a collection of data to Validat and let it handle the validation, sharing any number of custom validation rules across your application.

Reply to this Comment

@All,

So, if we have an external validation service for the individual beans, then what function to the beans serve? Are they, at that point, just objects that hold properties. It seems that if we can set bad data into them, to be validated at a later point, then the bean is really nothing more than a CFC that has a list of gettable and settable properties. If you have a base CFC as I do in my example, then really, we could quite literally have a CFC that has nothing but a list of VARIABLES.Instance = {} default values.

Something about this seems odd. I guess this is part of that ongoing debate between the "Anemic Domain Model" and "Smart Objets". At this point, why even have the CFC? Why not just have a Struct as the lightweight transfer object that can be validated later. It's not like I am actually using type checking on CFC type (methods that accept a CFC are always using type="any").

When I think about this, I can't help but think about what I discussed with Hal Helms, and the idea of "Idealized Objects" [ http://bennadel.com/index.cfm?dax=blog:1134.view ]. To me, this property-bean seems like it adds no value.

Now, while I am very new to OOP, I understand that the principle of "taking what changes and factoring out and encapsulating it" and the idea of a an external validation service really does go along with that point. Of course, is this one of those things that should be factored out from the beginning? Or is this something that needs to be factored out when it becomes an issue?

Reply to this Comment

@Ben -

In some cases I can see beans being as "dumb" as you mentioned with only basic getter/setter methods. But if you did not have the bean object, where would functionality like "getAge()" go when a Person only has a birthdate?

Beans can have a lot more functionality to them and I don't see validation logic as being the solution to making a bean non-anemic. One of the big goals of a good object model as I see it is to extract common code from multiple places (i.e. validation checks) and put it into one object or service so that it can be easily maintained.

Reply to this Comment

@Jeff,

Good point. I forgot about property-based methods.

Also, I took a look at the Validat examples on the Alagad blog; I didn't see anything about error messages - just validation "types". Do you define your user-friendly error messages outside of Validat?

Reply to this Comment

@Ben -

In the XML definition for Validat where you define which rules (assertions) should be applied to which fields (dataElements), thee is a property there called message. That message property can contain either plain english text or in most cases, I will have it return a resource bundle key that I can go to my localization setup and get back a string to display.

For example, the following snippet defines a data element named 'firstName' which is required and must also be between 1 and 100 characters in length.

... opps, can't paste XML into your blog ... check out this link instead to view the file in question: http://svn.alagad.com/validat/trunk/examples/formValidation1/_config/validat.xml (line 33-40)

Note, there is a message property on the dataElement tag which applies to the required state as well as another message tag within the parent assert. The second message is a tag because it is possible the assertion might return different messages for different events although the preference is usually to keep assertions pretty simple and reusable.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.