Form Helpers And Domain Objects As Data Types In Object Oriented Programming
As often as I feel setbacks in my research and development, I never want to give up my pursuit of object oriented programming in ColdFusion; as such, I continue to give it a lot of thought and lot of experimentation. After last week's discussions and practical object oriented programming, there were two things that I really wanted to try:
- Treating domain objects as data types that cannot be instantiated in a data-type-invalid state.
- Using Form helper objects to process form input and submission.
To experiment with both of these concepts, I created a tiny OOP-style application that allows me to Add, Edit, List, and Delete dates (romantic dates, as opposed to date/time stamps, which are slightly less romantic, but still pretty hot). This application alone is far too small to require object oriented programming; however, I am trying to lay the foundations of understanding for larger applications in which scalability and maintainability will be of crucial importance. If I can understand OOP on a small scale, I can then apply it on a larger scale where it really has a payoff.
For this experiment, the first thing I thought about was my core data type - my domain object, Date. For this, I created a Date.cfc ColdFusion Component that has the following properties:
- ID
- Girl
- Activity
- Date
As you will see in the following code, the Date.cfc's Setter methods validate the data-type-level requirements for these properties. These are not necessarily the requirements within the greater application - these are only the requirements needed for this object to exist in a valid state as a data type. Further validation will take place in other layers of the applications.
NOTE: I am using a single Getter method (GetProperties()) which returns a copy of the Instance variables. I wouldn't normally do this, I just didn't want to spend time on getter methods which are not a major requirement of the theories being tested.
<cfcomponent
output="false"
hint="I represent a date.">
<cffunction
name="Init"
access="public"
returntype="any"
output="false"
hint="I return an intialized object.">
<!--- Define arguments. --->
<cfargument
name="ID"
type="numeric"
required="false"
default="0"
hint="I am the unique identifier for this date."
/>
<cfargument
name="Girl"
type="string"
required="true"
hint="I am the name of the girl on the date."
/>
<cfargument
name="Activity"
type="string"
required="true"
hint="I am the date activity."
/>
<cfargument
name="Date"
type="date"
required="true"
hint="I am the date/time on which the date occurred."
/>
<!--- Define instance variables. --->
<cfset VARIABLES.Instance = {
ID = 0,
Girl = "",
Activity = "",
Date = ""
} />
<!---
Store the arguments (each Setter method will take
care of the validation and throw any exceptions that
are appropriate).
--->
<cfset THIS.SetID( ARGUMENTS.ID ) />
<cfset THIS.SetGirl( ARGUMENTS.Girl ) />
<cfset THIS.SetActivity( ARGUMENTS.Activity ) />
<cfset THIS.SetDate( ARGUMENTS.Date ) />
<!--- Return THIS reference. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="GetProperties"
access="public"
returntype="struct"
output="false"
hint="I return a copy of the instance properties.">
<!--- Return a duplicate of the properties. --->
<cfreturn Duplicate( VARIABLES.Instance ) />
</cffunction>
<cffunction
name="SetActivity"
access="public"
returntype="any"
output="false"
hint="I set the activity property.">
<!--- Define arguments. --->
<cfargument
name="Activity"
type="string"
required="true"
hint="I am the date activity."
/>
<!--- Check to see if this value is valid. --->
<cfif NOT Len( ARGUMENTS.Activity )>
<cfthrow
type="InvalidArgument"
message="The Activity argument you provided is not valid."
detail="The Activity argument you provided, #ARGUMENTS.Activity#, is not valid. Activity must be a string of length one or more characters."
/>
</cfif>
<!--- Store the proprety value. --->
<cfset VARIABLES.Instance.Activity = ARGUMENTS.Activity />
<!--- Return This reference for method chaining. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="SetDate"
access="public"
returntype="any"
output="false"
hint="I set the date property.">
<!--- Define arguments. --->
<cfargument
name="Date"
type="date"
required="true"
hint="I am the date/time the date occurred."
/>
<!--- Store the proprety value. --->
<cfset VARIABLES.Instance.Date = ARGUMENTS.Date />
<!--- Return This reference for method chaining. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="SetGirl"
access="public"
returntype="any"
output="false"
hint="I set the girl property.">
<!--- Define arguments. --->
<cfargument
name="Girl"
type="string"
required="true"
hint="I am the name of the girl."
/>
<!--- Check to see if this value is valid. --->
<cfif NOT Len( ARGUMENTS.Girl )>
<cfthrow
type="InvalidArgument"
message="The Girl argument you provided is not valid."
detail="The Girl argument you provided, #ARGUMENTS.Girl#, is not valid. Girl must be a string of length one or more characters."
/>
</cfif>
<!--- Store the proprety value. --->
<cfset VARIABLES.Instance.Girl = ARGUMENTS.Girl />
<!--- Return This reference for method chaining. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="SetID"
access="public"
returntype="any"
output="false"
hint="I set the unique ID property.">
<!--- Define arguments. --->
<cfargument
name="ID"
type="numeric"
required="true"
hint="I am the unique ID of the date."
/>
<!--- Check to see if this value is valid. --->
<cfif (ARGUMENTS.ID NEQ Fix( ARGUMENTS.ID ))>
<cfthrow
type="InvalidArgument"
message="The ID argument you provided is not valid."
detail="The ID argument you provided, #ARGUMENTS.ID#, is not valid. ID must be an integer."
/>
</cfif>
<!--- Store the proprety value. --->
<cfset VARIABLES.Instance.ID = ARGUMENTS.ID />
<!--- Return This reference for method chaining. --->
<cfreturn THIS />
</cffunction>
</cfcomponent>
Now that I have my data type in place, I created a Service object - DateService.cfc - that would deal with these Date objects and their persistence. For ease of use, I am not going to bother breaking my data access methods out into another object. The data is being persisted in a defined cache container as a ColdFusion query object. As you look at the code, take special note of the ValidateDateProperties() method. This method validates raw property values for use within the Date.cfc data type but outside of any particular instance. I found that this method was essential to cut down on duplication of data validation. You will see that both the DateService.cfc and the Form helper object rely on it.
<cfcomponent
output="false"
hint="I provide service methods for Dates.">
<cffunction
name="Init"
access="public"
returntype="any"
output="false"
hint="I return an intialized service object.">
<!--- Define arguments. --->
<cfargument
name="Cache"
type="struct"
required="true"
hint="I am the cache in which to store the data."
/>
<!--- Store a reference to the cache. --->
<cfset VARIABLES.Cache = ARGUMENTS.Cache />
<!--- Create a query to store in the cache. --->
<cfset VARIABLES.Cache.Dates = QueryNew(
"id, girl, activity, date",
"cf_sql_integer, cf_sql_varchar, cf_sql_varchar, cf_sql_timestamp"
) />
<!--- Return this reference. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="DeleteDate"
access="public"
returntype="any"
output="false"
hint="I delete the given date from persistence.">
<!--- Define arguments. --->
<cfargument
name="Date"
type="any"
required="true"
hint="I am the date being deleted."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Get the date properties. --->
<cfset LOCAL.Properties = ARGUMENTS.Date.GetProperties() />
<!--- Delete the given record. --->
<cfquery name="VARIABLES.Cache.Dates" dbtype="query">
SELECT
*
FROM
VARIABLES.Cache.Dates
WHERE
id != <cfqueryparam value="#LOCAL.Properties.ID#" cfsqltype="cf_sql_integer" />
</cfquery>
<!--- Return the deleted date. --->
<cfreturn ARGUMENTS.Date />
</cffunction>
<cffunction
name="GetDateByID"
access="public"
returntype="any"
output="false"
hint="I return the date at the given unique ID.">
<!--- Define arguments. --->
<cfargument
name="ID"
type="numeric"
required="true"
hint="I am the unique ID of the date."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Query for the date. --->
<cfquery name="LOCAL.DateRecord" dbtype="query">
SELECT
*
FROM
VARIABLES.Cache.Dates
WHERE
id = <cfqueryparam value="#ARGUMENTS.ID#" cfsqltype="cf_sql_integer" />
</cfquery>
<!--- Check to see if the date was found. --->
<cfif NOT LOCAL.DateRecord.RecordCount>
<cfthrow
type="RecordNotFound"
message="The given date could not be found."
detail="The given date with id, #ARGUMENTS.ID#, could not be found."
/>
</cfif>
<!--- Create a new date with given properties. --->
<cfset LOCAL.Date = CreateObject( "component", "Date" ).Init(
ID = LOCAL.DateRecord.ID,
Girl = LOCAL.DateRecord.Girl,
Activity = LOCAL.DateRecord.Activity,
Date = LOCAL.DateRecord.Date
) />
<!--- Return new date object. --->
<cfreturn LOCAL.Date />
</cffunction>
<cffunction
name="GetDates"
access="public"
returntype="query"
output="false"
hint="I return a query of all the dates.">
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Query for dates in proper order. --->
<cfquery name="LOCAL.DateRecords" dbtype="query">
SELECT
*
FROM
VARIABLES.Cache.Dates
ORDER BY
[date] DESC
</cfquery>
<!--- Return ordered dates. --->
<cfreturn LOCAL.DateRecords />
</cffunction>
<cffunction
name="SaveDate"
access="public"
returntype="any"
output="false"
hint="I persist the given date.">
<!--- Define arguments. --->
<cfargument
name="Date"
type="any"
required="true"
hint="I am the date being persisted."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Validate date object. --->
<cfset LOCAL.Errors = THIS.ValidateDate( ARGUMENTS.Date ) />
<!--- Check to see if we have any errors. --->
<cfif StructCount( LOCAL.Errors )>
<cfthrow
type="InvalidAction"
message="The given date cannot be saved."
detail="The given date cannot be saved due to errors in the following properites: #StructKeyList( LOCAL.Errors )#."
/>
</cfif>
<!--- Get the date properties. --->
<cfset LOCAL.Properties = ARGUMENTS.Date.GetProperties() />
<!--- Delete the given record. --->
<cfquery name="VARIABLES.Cache.Dates" dbtype="query">
SELECT
*
FROM
VARIABLES.Cache.Dates
WHERE
id != <cfqueryparam value="#LOCAL.Properties.ID#" cfsqltype="cf_sql_integer" />
</cfquery>
<!--- Check to see if we need to get a new ID. --->
<cfif NOT LOCAL.Properties.ID>
<!--- Query for max ID. --->
<cfquery name="LOCAL.MaxID" dbtype="query">
SELECT
MAX( id ) AS id
FROM
VARIABLES.Cache.Dates
</cfquery>
<!--- Store a new ID into properties and date. --->
<cfset LOCAL.Properties.ID = (Val( LOCAL.MaxID.id ) + 1) />
<cfset ARGUMENTS.Date.SetID( LOCAL.Properties.ID ) />
</cfif>
<!--- Insert the date record. --->
<cfset QueryAddRow( VARIABLES.Cache.Dates ) />
<cfset VARIABLES.Cache.Dates[ "id" ][ VARIABLES.Cache.Dates.RecordCount ] = JavaCast( "int", LOCAL.Properties.ID ) />
<cfset VARIABLES.Cache.Dates[ "girl" ][ VARIABLES.Cache.Dates.RecordCount ] = JavaCast( "string", LOCAL.Properties.Girl ) />
<cfset VARIABLES.Cache.Dates[ "activity" ][ VARIABLES.Cache.Dates.RecordCount ] = JavaCast( "string", LOCAL.Properties.Activity ) />
<cfset VARIABLES.Cache.Dates[ "date" ][ VARIABLES.Cache.Dates.RecordCount ] = CreateODBCDateTime( LOCAL.Properties.Date ) />
<!--- Return the persisted date. --->
<cfreturn ARGUMENTS.Date />
</cffunction>
<cffunction
name="ValidateDate"
access="public"
returntype="struct"
output="false"
hint="I validate the given date object for persistence.">
<!--- Define arguments. --->
<cfargument
name="Date"
type="any"
required="true"
hint="I am the date being validated."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Get the date properties. --->
<cfset LOCAL.Properties = ARGUMENTS.Date.GetProperties() />
<!--- Return validation errors. --->
<cfreturn THIS.ValidateDateProperties(
ID = LOCAL.Properties.ID,
Girl = LOCAL.Properties.Girl,
Activity = LOCAL.Properties.Activity,
Date = LOCAL.Properties.Date
) />
</cffunction>
<cffunction
name="ValidateDateProperties"
access="public"
returntype="struct"
output="false"
hint="I validate date properties outside of any date object.">
<!--- Define arguments. --->
<cfargument
name="ID"
type="string"
required="false"
default="0"
/>
<cfargument
name="Girl"
type="string"
required="false"
default=""
/>
<cfargument
name="Activity"
type="string"
required="false"
default=""
/>
<cfargument
name="Date"
type="string"
required="false"
default=""
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Create an error struct. --->
<cfset LOCAL.Errors = {} />
<!--- Validate ID. --->
<cfif (
(NOT IsNumeric( ARGUMENTS.ID )) OR
(Fix( ARGUMENTS.ID ) NEQ ARGUMENTS.ID) OR
(ARGUMENTS.ID LT 0)
)>
<cfset LOCAL.Errors.ID = {
Type = "InvalidProperty.Type",
Message = "The ID property is not valid.",
Detail = "The ID property which is, #ARGUMENTS.ID#, is not valid. ID must be positive a integer (including zero)."
} />
</cfif>
<!--- Validate Girl. --->
<cfif NOT Len( ARGUMENTS.Girl )>
<cfset LOCAL.Errors.Girl = {
Type = "InvalidProperty.ZeroLength",
Message = "The Girl property is not valid.",
Detail = "The Girl property which is, #ARGUMENTS.Girl#, is not valid. Girl must be a string of length greater than zero."
} />
</cfif>
<!--- Validate Activity. --->
<cfif NOT Len( ARGUMENTS.Activity )>
<cfset LOCAL.Errors.Activity = {
Type = "InvalidProperty.ZeroLength",
Message = "The Activity property is not valid.",
Detail = "The Activity property which is, #ARGUMENTS.Activity#, is not valid. Activity must be a string of length greater than zero."
} />
</cfif>
<!--- Validate Date. --->
<cfif NOT IsDate( ARGUMENTS.Date )>
<cfset LOCAL.Errors.Date = {
Type = "InvalidProperty.Type",
Message = "The Date property is not valid.",
Detail = "The Date property which is, #ARGUMENTS.Date#, is not valid. Date must be a date/time stamp."
} />
<cfelseif (ARGUMENTS.Date GT Now())>
<cfset LOCAL.Errors.Date = {
Type = "InvalidProperty.FutureDate",
Message = "The Date property is not valid.",
Detail = "The Date property which is, #ARGUMENTS.Date#, is not valid. Date must be past date/time stamp."
} />
</cfif>
<!--- Return the validation struct. --->
<cfreturn LOCAL.Errors />
</cffunction>
</cfcomponent>
Now that I have my data type and my service layer in place, let's take a look at the add/edit page for dates. This is really the main point of the experiment and where most of the functionality is leveraged. Let's look at the actual form input page first so you can see how the logic is factored out into the Form helper object:
<!--- Param ID. --->
<cfparam name="URL.id" type="numeric" default="0" />
<!--- Create a form utility. --->
<cfset objForm = CreateObject( "component", "SaveDateForm" ).Init(
DateService = APPLICATION.DateService,
ID = URL.id,
Form = FORM
) />
<!--- Create a default collection of errors. --->
<cfset arrErrors = [] />
<!--- Check to see if the form has been submitted. --->
<cfif objForm.Get( "submitted" )>
<!--- Validate form. --->
<cfset arrErrors = objForm.Validate() />
<!--- Check to see if we have any errors. --->
<cfif NOT ArrayLen( arrErrors )>
<!--- Process the date. --->
<cfset objDate = objForm.Process() />
<!--- Redirect to home page. --->
<cflocation
url="index.cfm"
addtoken="false"
/>
</cfif>
</cfif>
<cfoutput>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Simple ColdFusion OOP Demo</title>
</head>
<body>
<h1>
Add / Edit Date
</h1>
<form action="#CGI.script_name#" method="post">
<!--- Pass back submission flag. --->
<input type="hidden" name="submitted" value="true" />
<!--- Pass back ID. --->
<input type="hidden" name="id" value="#objForm.Get( "id" )#" />
<!--- Check to see if we have any errors. --->
<cfif ArrayLen( arrErrors )>
<p>
<strong>Please review the following:</strong>
</p>
<ul>
<cfloop
index="strError"
array="#arrErrors#">
<li>
#strError#
</li>
</cfloop>
</ul>
</cfif>
<p>
Girl:<br />
<input
type="text"
name="girl"
value="#objForm.Get( "girl" )#"
size="40"
/>
</p>
<p>
Activity:<br />
<input
type="text"
name="activity"
value="#objForm.Get( "activity" )#"
size="40"
/>
</p>
<p>
Date Occurred:<br />
<input
type="text"
name="date_occurred"
value="<cfif IsDate( objForm.Get( "date_occurred" ) )>#DateFormat( objForm.Get( "date_occurred" ), "mm/dd/yy" )#<cfelse>#objForm.Get( "date_occurred" )#</cfif>"
size="15"
/>
</p>
<p>
<input type="submit" value="Save Date" />
</p>
</form>
</body>
</html>
</cfoutput>
As you can see, when we get to the form input page, the first thing that we do is create an instance of the Form Helper object for this given form (SaveDateForm.cfc). The constructor for this objects requires the Date service layer, the FORM scope, and an initial ID (if we need to pre-populate the "edit" form). Once we have this form object, we use it to both Validate() and Process() the form data. When we render the form, we get the form values out of this form helper object rather than directly out of the FORM scope.
Now, let's take a look at the Form helper object, SaveDateForm.cfc, to see what logic is being encapsulated:
<cfcomponent
output="false"
hint="I am form utility for saving dates.">
<cffunction
name="Init"
access="public"
returntype="any"
output="false"
hint="I return an initialized object.">
<!--- Define arguments. --->
<cfargument
name="DateService"
type="any"
required="true"
hint="I am the service object for dates."
/>
<cfargument
name="ID"
type="numeric"
required="true"
hint="I am the default ID of the exist date (if there is one)."
/>
<cfargument
name="Form"
type="struct"
required="true"
hint="I am the submitted form scope."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!---
Set up the instance variables including default
form values (which may get overwritten shortly).
--->
<cfset VARIABLES.Instance = {
Form = {
id = 0,
girl = "",
activity = "",
date_occurred = "",
submitted = false
},
DateService = ARGUMENTS.DateService
} />
<!--- Append the given form. --->
<cfset THIS.SetForm( ARGUMENTS.Form ) />
<!---
If we have an ID, we might need to get an exist date
and populate the form. Only do this, however, if we
do not have an ID in the form already.
--->
<cfif (
ARGUMENTS.ID AND
(NOT THIS.Get( "id" ))
)>
<!---
Try to get the existing date (might throw
exception if date doesn't exist).
--->
<cftry>
<cfset LOCAL.Date = VARIABLES.Instance.DateService.GetDateByID( ARGUMENTS.ID ) />
<!--- Get the date properties. --->
<cfset LOCAL.Properties = LOCAL.Date.GetProperties() />
<!--- Move date propreties into form. --->
<cfset VARIABLES.Instance.Form.id = LOCAL.Properties.ID />
<cfset VARIABLES.Instance.Form.girl = LOCAL.Properties.Girl />
<cfset VARIABLES.Instance.Form.activity = LOCAL.Properties.Activity />
<cfset VARIABLES.Instance.Form.date_occurred = LOCAL.Properties.Date />
<!--- Catch any errors. --->
<cfcatch>
<!--- Date did not exist. --->
</cfcatch>
</cftry>
</cfif>
<!--- Return this reference. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="Get"
access="public"
returntype="any"
output="false"
hint="I return the given form data (or empty string).">
<!--- Define arguments. --->
<cfargument
name="Key"
type="string"
required="true"
hint="I am the form key being fetched."
/>
<!--- Check to see if the form key exists. --->
<cfif StructKeyExists( VARIABLES.Instance.Form, ARGUMENTS.Key )>
<!--- Return given value. --->
<cfreturn VARIABLES.Instance.Form[ ARGUMENTS.Key ] />
<cfelse>
<!--- Key not found - return empty string. --->
<cfreturn "" />
</cfif>
</cffunction>
<cffunction
name="Process"
access="public"
returntype="any"
output="false"
hint="Provided the form is valid, I process it and return the resultant object.">
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Create a new date object. --->
<cfset LOCAL.Date = CreateObject( "component", "Date" ).Init(
ID = VARIABLES.Instance.Form.id,
Girl = VARIABLES.Instance.Form.girl,
Activity = VARIABLES.Instance.Form.activity,
Date = VARIABLES.Instance.Form.date_occurred
) />
<!--- Save the date. --->
<cfset VARIABLES.Instance.DateService.SaveDate( LOCAL.Date ) />
<!--- Return populated date object. --->
<cfreturn LOCAL.Date />
</cffunction>
<cffunction
name="SetForm"
access="public"
returntype="any"
output="false"
hint="I set the form data.">
<!--- Define arguments. --->
<cfargument
name="Form"
type="struct"
required="true"
hint="I append the given FORM data to the internal form data."
/>
<!--- Apply the form data internally. --->
<cfset StructAppend(
VARIABLES.Instance.Form,
ARGUMENTS.Form
) />
<!--- Return this reference. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="Validate"
access="public"
returntype="array"
output="false"
hint="I validate the form data and return an array of user-friendly error messages.">
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Set up the array of error messages. --->
<cfset LOCAL.ErrorMessages = [] />
<!--- Get the raw errors. --->
<cfset LOCAL.Errors = VARIABLES.Instance.DateService.ValidateDateProperties(
ID = THIS.Get( "id" ),
Girl = THIS.Get( "girl" ),
Activity = THIS.Get( "activity" ),
Date = THIS.Get( "date_occurred" )
) />
<!--- Validate girl. --->
<cfif StructKeyExists( LOCAL.Errors, "Girl" )>
<cfset ArrayAppend(
LOCAL.ErrorMessages,
"Please enter a girl name."
) />
</cfif>
<!--- Validate activity. --->
<cfif StructKeyExists( LOCAL.Errors, "Activity" )>
<cfset ArrayAppend(
LOCAL.ErrorMessages,
"Please enter an activity."
) />
</cfif>
<!--- Validate date. --->
<cfif StructKeyExists( LOCAL.Errors, "Date" )>
<cfif (LOCAL.Errors.Date.Type EQ "InvalidProperty.Type")>
<cfset ArrayAppend(
LOCAL.ErrorMessages,
"Please enter a valid date."
) />
<cfelseif (LOCAL.Errors.Date.Type EQ "InvalidProperty.FutureDate")>
<cfset ArrayAppend(
LOCAL.ErrorMessages,
"Please enter a date in the past (cannot store future dates)."
) />
</cfif>
</cfif>
<!--- Return error messages. --->
<cfreturn LOCAL.ErrorMessages />
</cffunction>
</cfcomponent>
The main methods to take note of here are the Validate() and the Process() methods. The Validate() method passes the raw FORM data to the Service layer for validation - ValidateDateProperties(); notice that in this communicae, the form helper can translate the form field names into CFC-based names. The ValidateDateProperties() passes back a data-based error collection that contains the error types and programmer-specific messages. The Form helper object then takes this collection and translates it into an array of user-friendly, form-specific messages to be displayed in the current form.
Once the work flow has deemed that the form data is valid (no error messages are returned), the Form helper is asked to Process() the form. This creates an instance of the Date.cfc data type, passing in the appropriate, validated values to the constructor. It then asks the Date service layer (DateService.cfc) to save the given Date.cfc instance (SaveDate()), persisting it to the cache. Once this is done (and no exceptions have been raised), the user is re-located back to the main page.
So, that's the bulk of the experiment. I'm not quite convinced this is the right way to do things, so I'm really looking for feedback. I do like the concept of this ValidateDateProperties() method as a way to abstract out business-related logic in a way that it can be re-used by different objects. I suppose this could, itself, be factored out into some sort of a command object, but that's a fairly small step.
With all that out there, I'll just quickly outline the other pages in the application.
Application.cfc:
<cfcomponent
output="false"
hint="I define application settings and event handlers.">
<!--- Define application settings. --->
<cfset THIS.Name = "OOP-Testing" />
<cfset THIS.ApplicationTimeout = CreateTimeSpan( 0, 0, 10, 0 ) />
<cffunction
name="OnApplicationStart"
access="public"
returntype="boolean"
output="false"
hint="I fire when the application needs to be initialized.">
<!---
Clear the application scope in case we are
re-initializing manually.
--->
<cfset StructClear( APPLICATION ) />
<!--- Create a structure to cache our database. --->
<cfset APPLICATION.Database = {} />
<!---
Create a contact service singleton and cache it in
the application.
--->
<cfset APPLICATION.DateService = CreateObject( "component", "DateService" ).Init(
Cache = APPLICATION.Database
) />
<!--- Return out. --->
<cfreturn true />
</cffunction>
<cffunction
name="OnRequestStart"
access="public"
returntype="boolean"
output="false"
hint="I fire when the request needs to be initialized.">
<!--- Check to see if we are manually initializing. --->
<cfif StructKeyExists( URL, "reset" )>
<!--- Manually execute application initialization. --->
<cfset THIS.OnApplicationStart() />
</cfif>
<!--- Return out. --->
<cfreturn true />
</cffunction>
</cfcomponent>
Index.cfm:
<!--- Get the dates. --->
<cfset qDates = APPLICATION.DateService.GetDates() />
<cfoutput>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Simple ColdFusion OOP Demo</title>
</head>
<body>
<h1>
Date Records
</h1>
<table cellspacing="2" cellpadding="5" border="1">
<thead>
<tr>
<th>
Date
</th>
<th>
Girl
</th>
<th>
Activity
</th>
<th>
Action
</th>
</tr>
</thead>
<tbody>
<!--- Check to see if we have any records. --->
<cfif qDates.RecordCount>
<cfloop query="qDates">
<tr>
<td>
#DateFormat( qDates.date, "mm/dd/yy" )#
</td>
<td>
#qDates.girl#
</td>
<td>
#qDates.activity#
</td>
<td>
<a href="edit.cfm?id=#qDates.id#">Edit</a>
-
<a href="delete.cfm?id=#qDates.id#">Delete</a>
</td>
</tr>
</cfloop>
<cfelse>
<tr>
<td colspan="4">
<em>There are no dates.</em>
(<a href="edit.cfm">add date</a>)
</td>
</tr>
</cfif>
</tbody>
</table>
<p>
<a href="edit.cfm">Add new date</a> »
</p>
</body>
</html>
</cfoutput>
Delete.cfm:
<!--- Param the URL. --->
<cfparam name="URL.id" type="numeric" default="0" />
<!--- Try to get the given date. --->
<cftry>
<cfset objDate = APPLICATION.DateService.GetDateByID( URL.id ) />
<!--- Delete the date record. --->
<cfset APPLICATION.DateService.DeleteDate( objDate ) />
<!--- Catch any errors. --->
<cfcatch>
<!--- Perhaps the date didn't exist. --->
</cfcatch>
</cftry>
<!--- Relocate to homepage. --->
<cflocation
url="index.cfm"
addtoken="false"
/>
Thank you for any and all feedback you might have.
Want to use code from this post? Check out the license.
Reader Comments
Seeing as Form Helper objects have a one-to-one usage for forms, I am not sure that they add any value of just having the same logic directly in the controller. Are we factoring functionality out of the Controller for no reason?
If you follow the "single responsibility" principle (leading to high cohesion), you want to limit the Controller to handling application flow. It's the "traffic cop". Shoving validation in there dilutes its cohesion and increases the reasons why it would need to change.
@Brian,
I gotcha. I see what you're saying.
Ben, have you looked at "metro" http://metro.riaforge.org/?
I am pretty sure it attempts to solve similar issues. It also uses jQuery for client side validation, thought you might like that.
I would be interested on your thoughts on "metro" if you get to it.
I just started looking at it myself.
WOW! LOL... you are trying to scare proceedural coders away there aren't you!
@Ilya,
Looks a bit out of my league at the moment. Uses Transfer and ColdSpring... I'm still trying to learn the basics :)
@John,
I'm just trying to figure out what the heck I'm doing :)
Or, if it's appropriate to factor the validation out of the Controller and into the business layer (the Model), then wouldn't the (server-side) validation all be moved into the DateService.cfc, as the primary representative of the Model?
Just thinking out loud here, too ... thanks for the continued explorations!
@Jason,
Right now, in this experiment, the validation is really in the Service. Only the Form Helper calls the service method for validation.
Maybe it'd be easier if u just draw up some UML class diagrams?
@Henry,
I can list out the objects, but there really are no inheritance, composition, or aggregation relationships (maybe one or two). I don't think a UML diagram would add much understanding.
Date.cfc
-------------------
- ID
- Girl
- Activity
- Date
-------------------
- GetProperties()
DateService.cfc
-------------------
- DeleteDate()
- GetDateByID()
- GetDates()
- SaveDate()
- ValidateDate()
- ValidateDateProperties()
SaveDateForm.cfc
-------------------
- Process()
- Validate()
I left out some non-essential things like property Setter / Getter methods. I am not sure that this is helpful - I think the meat of the concept is how they interact in the details.
A sequence diagram would really help.
I made one for you, pls take a look if I made a mistake:
http://www.websequencediagrams.com/?lz=Q29udHJvbGxlci0-b2JqRm9ybSA6IGNyZWF0ZSBTYXZlRGF0ZUZvcm0KYWN0aXZhdGUgACAHCgAlFnZhbGlkYXRlKCkKAEkHLT5EYXRlU2VydmljZQAXCkRhdGVQcm9wZXJ0aWVzKCkKABsLAIEDCTogcmV0dXJuIG5vIEVycm9ycyAAYRdwcm9jZXNzAGsMb2JqRGF0ZToAgUcHRACBNgdlY3QAgUANRGF0ZQoAJQcAaBEAgTsKAIIgCgCBDAkANQhkZXN0cm95AIIHEyAtPgBbCDogdmlldyAvIGVkaXQgaXQK&s=omegapple
Worth havig SaveDataForm class around? IMO, I don't think so.
I would rather skip it and ask Controller layer to unpack the FORM properties, call Service directly for validation and object creation.
@Henry,
That seems like a bad-ass service! Looks like it makes creating workflow diagrams really easy! I'm glad to know about that. I like these things - they feel more useful than UML diagrams (I guess its really a different purpose).
RE: having a SaveDateForm.cfc... I am torn. My first reaction is to agree with you; but, then I think about what Brian Kotek is saying above about the "single responsibility" principle.
But that said, single responsibility principle or not, DO I actually get any value out of having the additional CFC especially considering it's a one-to-one radio of form interfaces to form helpers? Other than the "concept" of it, I cannot think of a great reason.
@Ben
which "Single Responsibility" principle is violated?
Controller's responsibility is to unwrap FORM fields and talk to Service to carry out an event, right?
@Henry,
I believe Brian was explaining that the Controller should just handle work flow and not validation... but reflecting on that, I might not have explained myself properly. The Controller is not really doing any validation; as you say, it's simply unpacking FORM data and making calls to the Service layer. The Controller isn't actually doing any validation.
Hmmmmm. Liking the idea of a Form Helper less and less.
In my experience, one of the primary purposes of a controller is exactly that - interpreting user responses and then making appropriate delegation to the service layer for things like validation, workflow, etc.
IANAG*, but that's my two cents.
* I Am Not A Guru
@James,
Yeah, I am leaning that way now.
We might have to be careful defining what the "controller" is in practice. Often times, a framework coupled with configuration xml files are defined as the controller. It simply maps the model and the view together. In cases like that I've found that pushing the logic into a separate object, a service object for the page or section of the site makes life easier. This object has methods that just handles the business logic for a particular page and calls the other objects as needed. So if I was on a "create date' page, I would have a "createDate" method in this component. I would handle the form validation here. There are still opportunities for code reuse here, validating a form for both create and edit pages for example.
You might consider this object part of the Controller, the point is that the line gets a bit blurred when it comes to the naming terminology.
@PJ,
Right, the lines do start to get blurry. And, I think once you have your Controller making more than one or two calls to the Service layer, then it really is performing work flow / business logic rather than just passing it off to the Model.
Now, with these blurry lines, I have to wonder, does the overhead of creating separate objects even become worth it? I'm gonna do some more pondering on this then sum up my thoughts.
That's just it, the "site section/page" service layer I'm talking about is just called once by each page. Basically a method is created specifically for that page and I pass in a reference to the variables structure so I can actually manipulate values for the page directly. It's really not so different than including another file just for the business logic of top of the page, but there are some advantages to using a component instead.
There might be situations where you're stringing pages together, like for some kind of navigation bar, where technically the service layer gets called more than once, but still, only one method per "piece" of the page gets called. This method handles all the business logic for the page and calls any other methods it needs from either itself (validateDateForm()) or other objects (Date.SaveDate()).
Advantages I see for doing this are: it separates the business logic from the view, provides the possibility of reusing some code (process/validate form), and performance since it can be used as a singleton and pushed into the application scope.
Question: Since we're talking practical OOP now...
My biggest hurdle when it comes to OOP and CF is when deciding when it makes sense to use a real object like your Date object, where you use getters and setters to set it's values. I've tried in the past and ran into performance issues, but maybe I just wasn't doing it right.
I ran some tests way back in the day and it's way faster creating a structure than it is to instantiate a new object and it takes up far less memory. Likewise, getting/setting a value from a structure is far faster than going through a method. Converting a query to an object is adds expense, even if it's one record, let alone some sort of array of objects, even if there are tools that exist that help do it for you. Plus they were harder to work with IMHO.
Trying to figure out where in the sand to draw the line on OOP is a case by case basis I guess, but even so, each case is tough!
PJ,
First off, I don't think you can ever create a Form helper as a singleton as it is defined to hold form data. If it were a cached singleton, you would have multiple overwriting each others data when they start the form process (perhaps in parallel).
As far as reuse on things like validation, I am not sure how reusable that actually is as in my example, the form helper isn't really doing any validation - it is just passing off raw data to the Service layer. The only thing the form helper does is translate the property-based errors returned by the Service layer into user friendly errors returned to the client. This kind of thing cannot really be re-used as it is form-specific.
As far as keeping the View and the Business logic separate, I think you can do this without it being a CFC. Remember, if all you are doing is moving procedural code into a CFC and then making calls to it... you're not really separating View and Business logic, you're just moving it procedural calls rather than inline.
As for the slowness of object creation, that is real and is why people return queries when relevant and use CFCs only for single objects or small collections of objects.
It can be in a singleton since the service method for the page will not persist any data.
<cfscript>
appliation.ContactPages.create(variables);
</cfscript>
So the method is persisted, not the values you pass to it. In your example, you would have to pass in a form struct reference as well.
Lets use your example:
From controller or view:
<cfscript>
appliation.DatePages.create(form);
</cfscript>
Service method in DatePages component:
<cffunction name="create"....
<cfargument name="form" ...
<cfscript>
var local = structNew();
local.errorStruct validateForm(form);
if(structCount local.errorStruct EQ 0) //create date object and use dateservice to save
</cfscript>
</cffunction>
<cffunction name="edit"....
<cfargument name="form" ...
<cfscript>
var local = structNew();
local.errorStruct validateForm(form);
</cfscript>
</cffunction>
<cffunction name="validateForm"...
</cffunction>
@PJ,
I am not sure we are talking about the same thing :) I thought you were referring to the form helper object, which holds form data. Yes, you can and should cache service layer objects.
Gotta jump on a call, but I'll take a look at your code in a second.
I don't understand why CF developers seem to be so determined to avoid custom tags for view oriented code. Not for all of it but packaging view code in custom tags is very effective. We are all familiar with the concept because that is exactly what happens in the basic realm of HTML. The dom is still created but the markup is easier to think, package and manage with tags.
Now if you go to the next level and mix the (VIEW) with the (LOGIC) in a system of integrated tags and objects it is a powerful thing! We do this and it helps us greatly. It makes integrating technologies like AJAX much easier and in fact we find a great similarity in the joy of software development that comes from coding in Flex to the joy of coding this way in CFML.
@ben: It's not really a form helper object per/say, it's a service method for the page itself that handles the form. Again, this might just be part of the controller in you view.
Last example was a bit rushed so here's another attempt to illustrate the concept. You should be able to see how validateDateForm might be able to be reused...
<!--- Controller: Juts a cfml page for this example. --->
<cfscript>
application.DatePages.create(variables, form, successPage);
</cfscript>
<cfinclude template="date/createDateView.cfm" />
<!--- DatePagesService: DatePages Compoenent --->
<cfcomponent>
<cffunction name="create" access="private" returntype="void"
hint="I am the service layer for the create date page.">
<cfargument name="vars" type="struct" required="yes" />
<cfargument name="form" type="struct" required="yes" />
<cfargument name="successPage" type="string" required="yes" />
<cfscript>
var local = structNew();
if(isDefined('arguments.form.fieldNames')) {
local.ErrorStruct = validateDateForm(arguments.form);
if(structCount(local.ErrorSTruct) EQ 0) {
// Create New Date Object
// Store Date Object with DateService
// Go to arguments.successPage page
} else {
arguments.vars.errorStruct = local.ErrorStruct;
}
}
</cfscript>
</cffunction>
<cffunction name="validateDateForm" access="private" returntype="struct"
hint="I can be used to validate a date form.">
<cfargument name="form" type="struct" required="yes" />
<cfscript>
var local = structNew();
local.ErrorStruct = structNew();
// Validate form and populate error struct with errors.
return local.ErrorStruct;
</cfscript>
</cffunction>
</cfcomponent>
@Ben To illustrate my point of having a Form object with methods like display, populate, and validate, I posted a blog entry showing how I'd handle it.
Oh yes: http://halhelms.com/blog
@John,
I agree! Custom Tags are awesome things! Especially for modularizing view features.
@PJ,
I think I see where you are going with that. One question though - how do you handle form initialization? Meaning, if I come to the page with a ContactID (for example), where do you load the initial contact information into the form? Is that part of your Controller?
@Hal,
On my way to check it out!
Ok, so say it turns out your date's sister is hotter so you need to edit your Date. On the edit date page, since the controller just passes the variables/form/url data to the DatePages.edit() method, the "date_id" would arrive as well since it's in the form scope. If we were giong OO all the way, we could then create a DateBean and pass that to the DateDAO to populate. However you want to do it, point is, you grab the data and just put it in the form scope.
arguments.form.girl = local.Date.getGirl();
@PJ,
Since we are still interacting with the FORM data, we are not yet in the "object model". We are in the View world. Really, I see the Form helper as an extension of the Controller / View-Extension. Therefore, its job is to translate the simple form data (IDs and what not) into objects or, in our case, service calls.
The Service layer is still using the domain model, but the Controller / Form helper knows how those relate. As such, I don't think it is necessary to use the domain model in the Form helper.
While I am just thinking this all out, I feel like that stuff should stay inside the service layer.
@Ben
I agree with you, Ben. We're not in the object model at all. In fact, all this can be done without ever using domain objects, if that's appropriate for the application. Really, this stuff isn't that hard!
I wish I had gone to one of those OOP sessions you guys had (on the other side of the world right now) so I could better understand your terminology. I'm having trouble deducing what you guys define as the object model, domain object, etc., even after following along your posts. Still find it all fascinating thou!
I see the form/helper as half controller, half model. It seems to contain logic for both. I think that when developing web applications you'll always need a layer to tie the view with real objects as well as provide additional logic for the page. In .net there are code behind pages, in CF we have (have the option of) figuring out where to put it ourselves.
I know the Mach II framework they put this "logic" in their "Listener" components, least when I was using it a couple years back. There's a guide out there made by SC for mach-ii while he was still at Adobe that you might find interesting since it covers a bit about how the go about using OOP when they were making a few things over there.
You may have seen this already but here's the link:
http://livedocs.adobe.com/wtg/public/machiidevguide/
BTW, I'm not evangelizing this particular framework, just wanted to provide additional material on the OOP/CF subject.
Ben,
I'm following your OO progression here with great interest, probably because I went through a long period of struggle to try and understand "OO" on both an abstract and practical level. It consumed probably about a year or more of time. Everything I produced in that period is ... well, neither suitable for scalable production or as a model to use for further development. Junk. I learned something by "living within" the mistakes I made, but I did it the hard way.
I think there are 2 separate things to learn in regards to OO. The first you already know, and that is the basic mechanisms of OO, objects, methods, messages between objects, and the mechanics of object creation, composition and inheritance. If that's what your code is comprised of, then it's orientated around objects in the simplest sense of the term. I wouldn't discount that, because in my opinion, that's powerful in itself.
The second is learning OO architecture, and that's very different in character. There is no end to that pursuit, and a very wide variety of approaches are used. Those who have significant OO experience will recommend very different paths. If one's reputation as a developer is staked on an OO development approach, arguments for that approach and against others will likely be pursued with persuasive vigor. And I've noticed over the years that some experienced OO developers seem to have changed their architectural approach. It can take a long time to be able to discern which path you prefer, and even then, the preference is likely temporary.
So in essence, what I'm saying is that the uncertainty you express in between the lines of these recent posts regarding OO architecture most likely won't go away for a long time.
In your post, you posit "If I can understand OOP on a small scale, I can then apply it on a larger scale where it really has a payoff." It's a reasonable assumption, but as your Dating application grows, the architectural problems you will find yourself needing to solve will change and become more complex, the possible approaches you might use increase, and the codebase you need to modify to implement those learn-as-you-go changes becomes ever larger. Nothing wrong with trying to solve a small part of the puzzle, but in my experience, that doesn't necessarily lead to solving the whole puzzle.
In the last week or two, I've decided to learn jQuery. What everyone understands by that is I'm learning to use the jQuery framework. But I could also declare "No, that's not what I mean. I want to learn how to create a framework like jQuery" - and that's an entirely different thing, orders of magnitude more difficult.
If I solicited your advice as someone relatively more experienced than me, you might suggest to learn to use the framework first to a get a good sense of how it works. In fact you might suggest to learn several Ajax frameworks, so I could experience the benefits and drawbacks to each of the architectures that others have created.
I think we all need to decide what our actual objective is in learning OO. Do you want to learn to become an expert architect of OO systems? Or do you simply want to learn to use OO well enough to leverage existing frameworks? Which applies practically to one's career path and character?
Even if you decide that your goal is to learn OO architecture, I would still offer that perhaps the best way to go about it is to learn to use other's frameworks first. Live in their architectures, and see what you prefer and what you don't. After using it, break open the framework code and spend some time studying it.
One of the most powerful aspects of OO is that it allows one to easily use an API that others have developed. You don't have to do everything yourself. That's precisely what makes jQuery so compelling, the abstraction layer it provides alike to JS experts and dunces (like myself). If you one day become an expert OO architect, you will almost certainly provide an API so that other developers can utilize your codebase.
I currently use a Coldspring, Transfer, ModelGlue stack to develop in. Because I'm leveraging these APIs, the code (that I would need to write) for a comparable Dating application in this stack would be far simpler than you have it, and the framework/OO environment it was established in would provide me with a clear guide how to extend the Dating application in whatever way the woman I was dating and the evolving situation demanded. I stop worrying about architecture and focus on solving my real-life problem, such as tracking "whose turn is it?" (It's obviously my turn to take out the garbage, because all the bags were piled out the door in my way this morning. ;-)
And I could hand off the app to anyone else, regardless of their experience with the frameworks, and they would have the necessary documentation to quickly pick up where I left off, because it's all based on established APIs.
Perhaps someone would disagree with the approach to use frameworks as a learning tool, and perhaps with valid arguments that I could even agree with. But I still think there is scope for examining others' approaches to solving OO architectural challenges by living in their solutions for awhile to get a sense of what you like or don't like in the choices and tradeoffs they made. OO architecture is such a complex topic, that practically speaking, it seems prudent to utilize others' experience and knowledge as much as possible, rather than to try too hard to start from scratch.
That said, a bit of starting from scratch is good. It lets you "experience the pain" necessary to understand an OO solution. But I also think that utilizing and exploring others' solutions in practice, the OO frameworks that are already developed, can be an important component to the learning experience.
And again, I think we all need to consider if we simply want to learn to use OO APIs that others have developed (like I'm learning to use jQuery - there's nothing wrong with only knowing enough to use the framework - it depends on your goals) ... or if we want to become an expert OO architect. It's not a black or white choice, but it may be that beginners don't realize that using OO frameworks in CF is quite simple. The term Object Orientation has somehow developed a mystical connotation that it doesn't deserve. Very little, if any knowledge of OO architecture is necessary to use OO in CF by leveraging an OO framework, just as I need very little knowledge of high level Javascript concepts and programming techniques to get a lot of mileage out of jQuery.
In fact, that's the whole point of jQuery! I should repeat that.
The point is that I can do a lot knowing very little. An abstraction layer provides a simple way to do powerful things.
The same can be said for ColdFusion OO frameworks, and for me, that's the key reason for using them. ColdSpring solves the nasty problem of object creation/dependency. ModelGlue does what it sounds like, provides a means to glue the model to the view by providing a consistent framework to develop and precisely manage the wiring in the middle. And Transfer solves the problem of persisting objects to the database while at the same time providing a means to generate, instantiate, cache and extend business objects with decorators.
Now I'm going to get a coffee and try to get rid of those nasty sort arrows by implementing jQuery's sortable plugin! And I'm not going to even think about what might be behind that interface. Whether or not my approach is "valuable" depends entirely on the context of my own, and by extension, my client's goals. I believe the same can be said regarding how I approach OO in ColdFusion.
I totally refactored this concept to enable reuse of server-side validation on the client via AJAX calls and progressive enhancement. Now, it starting to really take shape (and feel useful):
www.bennadel.com/index.cfm?dax=blog:1557.view
@Nando,
That's a very thought-provoking comment. I think, the reason I want to learn object oriented programming is because I want to learn better programming practices. Now, whether it is better to learn something indepth, or learn how to leverage an existing framework to get your work down... I am not sure that these are the same goals. I think one can be used to get the other, but I don't think that they solve the same problem.
Take jQuery for example. I use jQuery a LOT. It think it's fantastic. And yes, you can get a lot of mileage out of it without having to know very advanced Javascript. But, it's not a replacement for advanced Javascript. The more I learn about advanced Javascript, the more powerfully I am able to leverage the jQuery library. The two skill sets compliment each other - they don't replace each other.
However, I think this is an odd statement, almost an oxymoron:
"I currently use a Coldspring, Transfer, ModelGlue stack to develop in. Because I'm leveraging these APIs, the code (that I would need to write) for a comparable Dating application in this stack would be far simpler than you have it"
I am not sure that an application that requires 3 frameworks can be considered simpler than a demo that has about 8 files. How can something be both more simple and at the same time require much more complexity to run?
And what is more simple? I would be interested in seeing that (as I don't have much experience with the above frameworks). Where do you keep your validation? How do you ensure that objects cannot be created in invalid states?
I am not attacking, I am actually curious! I don't see how much more simple this can get (even thought it is complex).
If you think it would be fun, build a version of this that is powered by your frameworks so I can see it. It would be a great learning tool for me because, like I said, I don't really have any experience with the above frameworks.
Hey Ben,
So, first, let's address this issue of "simple". Of course you need to know how to use the frameworks and set them up. That's not simple at first, you need to learn them. Same with jQuery. Show me those $'s and chained methods for the first time and I'll be lost. You're not lost anymore with jQuery. It's simple for you. So allow yourself, in your imagination, to come to the same point as you are with jQuery, with this ColdSpring, ModelGlue, Transfer stack. You'd just need to sit down for a few days and putter with them and it would click into place.
Here are the critical snippets of code you would need:
Here's your Transfer config:
<object name="MyDate" table="MyDate" decorator="model.MyDateDecorator">
<id type="numeric" name="myDateId" generate="false" />
<property type="string" name="girl" />
<property type="string" name="activity" />
<property type="date" name="theDate" />
</object>
ColdSpring injects an instance of Transfer into your MG controller, so anywhere within your controller, you'd call the following code to get an new, unpopulated instance of a MyDate object:
<cfset myDate = getTransfer().new("MyDate") />
or a persisted instance of myDate from the database
<cfset myDate = getTransfer().get("MyDate", arguments.event.getValue("myDateId")) />
You'd have a method in your controller, I'd call it "getMyDate", that would place either a new or persisted instance of myDate in the event, depending on whether an id was present in the event coming into the function. Here we place an instance of myDate into the event.
<cfset arguments.event.setValue("myDate", myDate) />
myDate is generated by transfer from the config information above. You don't have to write that. Getters and setters for all properties are available on the object, plus a host of other methods. That may seem trivial, but hang on a moment. Let's modify the Transfer config a bit and make this more interesting.
<object name="Date" table="Date" decorator="model.DateDecorator">
<id type="numeric" name="dateId" generate="false" />
<property type="string" name="activity" />
<property type="date" name="date" />
<onetomany name="Girl">
<link to="Girl" column="girlId"/>
<collection type="array" />
</onetomany>
</object>
<object name="Girl" table="Girl" decorator="model.GirlDecorator">
<id type="numeric" name="girlId" generate="false" />
<property type="string" name="name" />
<property type="string" name="hairColor" />
<property type="numeric" name="dateOfBirth" />
</object>
Now you can go out with more than one girl at a time on a date. Now when you call:
<cfset myDate = getTransfer().get("MyDate", arguments.event.getValue("myDateId")) />
myDate has an array of girls within it which you can access via the Transfer API.
To get those girls saved to a particular date, you'd need a form that would allow you to add them. Let's assume the app asks you the question "How many girls did you take on your date last nite?" You answer "2", and the app gets a new myDate instance with an array of 2 girls within it. Your form is designed to loop over the array and render the necessary fields. You can either fill in a new girl or choose a girl you've already dated. You submit the form. Validation happens (we'll get to that in a moment). In your controller, here's the line of code you need to persist myDate:
<cfset getTransfer().cascadeSave(myDate) />
So where does validation go? Basically, in or via the decorator objects declared in the Transfer config. Before we go into that, you'd put your getAge() method for Girl in GirlDecorator, and it would be merged with your girl transfer object, so you could call getMyDate().getGirl(1).getAge() to see if you could take girl[1] out to a bar or not, for instance.
You'd also want to set up an abstract decorator that all of your decorators extend, and inject your Validation object into that decorator. You'd then call your validation method as necessary from within the transfer object itself, something like
<cfset myDate.validateMe()>
Depending on how you want to set that up, validateMe() could either overwrite the abstract method (in the abstact decorator) or not, depending on requirements. A generic populate method would also reside in your abstract decorator. Brian Kotek also advocates injecting an instance of Transfer into the abstract decorator so you can do this:
<cfset myDate.save() />
The only code you'd write would be the Transfer config, the form, and the validation rules in the decorators, plus the wiring and API calls.
Transfer in particular is quite powerful. I deal with composed objects extensively, because the applications I work on are often multilingual.
That's not a complete application, but I hope it gives you an insight. There is sample code particularly at http://docs.transfer-orm.com/wiki/Example_Code.cfm .
Now I should get to bed!
@Nando,
Very interesting stuff. Thank you for taking the time to explain all that. I think I follow most of it.
But, just to play devil's advocate - since this post is about using CFC as "Data Types", what happens when call:
<cfset myDate = getTransfer().new("MyDate") />
Assuming that in order for this "Data Type" to be valid, it has to have a valid date/time stamp in the "date" field, does Transfer populate the "date" field with a default date?
My guess is that it does not (and that that is what the ValidateMe() method would report).
Assuming I am not off on the above assumption, I guess the Big Question I have to ask myself is, Does treating CFCs as data types help or hinder development?
It seems that so many of the frameworks and the "OO" that people are doing does not treat CFCs as data types.
"does Transfer populate the "date" field with a default date?"
Yes it does, I believe it defaults to now(). It also handles null() translations in and out of the database, allowing you to set the null value you want to use for numeric, date, and string fields. You can also set your own default values. From the documentation:
By default, Transfer sets values to properties that are found on TransferObjects.
These default values are:
* string: ""
* numeric: 0
* boolean: false
* date: Now()
* UUID: a random UUID
* GUID: a random GUID
* binary: an empty byte array
To see more on how to set your own defaults, see the 'configure' method in the Custom Methods or Writing Decorators sections of the documentation.
If defined, the configure method is run in the decorator when a transfer object is created, and that's where you can define your own default values.
I'm not sure if it's terribly important that an object be "treated" as a datatype. I haven't really grasped the argument, other than Hal using this way of thinking to say, "Look, it's not as complex as it might seem. An object is the same as any other datatype, only with the added feature that it can also do things ...".
But I will say that HTML forms treat all data as strings, so there is no concept of a date or numeric type there. When you attempt to plug typeless HTML data into an object with typed properties, you have a potential problem. If I'm building my business objects by hand (which I generally don't do anymore, I'd use Transfer instead) then one way of approaching this problem is to not type the arguments on the setters, so string data is allowed, and then run validation on it to make sure CF can translate it into a numeric or date type.
Another way of approaching the problem is the place an object, a formHelper of some sort in between the form and the business object, to do the translating and initial verification for you. But then as your application expands to include, say, 50 business objects, you get an explosion of classes to deal with, and you probably won't like that. At first it might seem to make sense to add objects for these things, but as we become more experienced (as a community as well as individually) I think we find that it's better to have capable business objects that can verify their own data and even know how to persist and delete themselves.
So how I would approach "Date" architecturally is to make it quite capable of handling its own affairs, and that may include giving the flexibility where necessary to move between the world of HTML data, all strings, and the valid state necessary to persist it to the database.
Since Transfer will type its getters and setters, I would provide the flexibility in the decorator, setting a potentially non-numeric value in an extra property that I would hand code, validating it as numeric for instance in my validation routine, and then setting the correct property using the Transfer generated setter. Where I can filter that out tho' within the form, by using a Date picker for instance, I will.
I should say one more thing. As an American born person living in Switzerland, I _very_ often run into forms in US based websites that don't allow me to enter my valid postal address or telephone number because of their validation routines. It drives me nuts, and actually has cost me hundreds of dollars and many days of time trying to sort the messes out. Believe it or not, one of the worst offenders has been and continues to be Adobe! All it takes is one validation happy programmer fixed on data types and lengths to make it impossible for me to enter my valid data. I have to go to great lengths to purchase software from Adobe. I won't even try anymore. One of many details: the online form doesn't allow me to enter my VALID billing address in several places and when I try to fudge it, the credit card transaction doesn't go though. Of course, I can't enter my valid shipping address either, but since I can't purchase it anyway, it doesn't make any sense to try and send it to a friend in the states! It's all very frustrating from my perspective as a client. I NEED Adobe software sometimes to do my work, and I simply cannot purchase it from them. Calling doesn't help at all, they use the same form in the call center!!!
So I'm liberal and user centric when it comes to validating data.
And I also reserve CF's error handling for genuine server errors. A validation method begins with
<cfset var isValid = true />
and has conditional tests within it that set isValid to false and any messaging or messaging flags necessary rather than try/catch blocks. And there are as few validation tests as absolutely necessary. Telephone numbers and postal codes are all strings of generous length for instance. I keep in mind that I'm dealing with people on the other end of those forms, not databases, and I have no way of verifying what a VALID phone number or zip code is for a particular person.
So both because of typeless HTML and the fact that people should not be treated as if they were databases, and because you probably want capable business objects rather than suffer from an object explosion, in real world web application development you might want to allow some flexibility in terms of data types. Think it over.
"Does treating CFCs as data types help or hinder development?"
If that treatment is inflexible within the context I've outlined, then my take on this would be it can definitely be a hindrance, but I may not understand fully what you mean. For all the reasons I've outlined, I'd go for flexibility over rigidity.
@Nando,
I certainly agree with you as far as validation-happy programmers go. I absolutely hate it when people what to tell me how to validate a phone number or an address. The WORST is people who force you break up your phone number into different fields (3 + 3 + 4).... are you serious? It's like they put some imaginary importance on being able to parse out pieces of information that hold no value!
The other thing I hate is when people put date masks on input fields. Do you know HOW MANY date formats ColdFusion can accept as a valid date? Loads! If I feel more comfortable putting in Apr 1, 2009, don't you *dare* make me enter it as mm/dd/yy ... just let me enter a date that's valid and YOU freaking figure out how to store it in what ever way makes you happy.
Uggg, ok, rant over :D Thanks for letting me get that off my chest.
Nando, it seems like Transfer is really cool. I was not aware that it would handle as much functionality as you have outlined.
I'm not sure I would go so far as to say that since forms submit values as strings, then objects should just accept strings. To say that would be to say that the primary job of the form is to populate business objects, which is not the case; the primary job of the form is to get information from the user. What we do with that information is up to the business logic. Of course, this might be an argument of principal, not of practicality.
Also, I don't think there would be a "class explosion" of form helpers if we had 50 business objects. Form helpers go hand in hand with forms, not business objects. Plus, a 1:1 ratio I don't think can ever be considered a "class explosion".
That would be like saying there was a "form explosion" every time we needed to gather more information from the user.... I'm all for finding the easier way, but I don't want to make fear-based arguments (ie. the idea of "class explosion" sounds very scary but is, in fact, not a reality).
Thank you for taking the time to respond so thoroughly - you have given me MUCH to think about.
Transfer IS really cool. I've only touched the surface here. I think Hal may have an argument against using it, but I'm not sure what it is yet. I only saw a comment on his blog, "Don't get me started on Transfer". My guess is that his argument may be that the design of your model shouldn't be dictated by the design of your database. Sounds right to me, but so far, I haven't had to worry about that in the types of applications I develop and given my limited capability as an OO architect. So the somewhat "happy in my ignorance" bottom line for me is that Transfer helps me to be much more productive, eliminating a huge amount of code that I would otherwise have to write.
I could also say that you are not forced to limit the business objects in your architecture to transfer objects.
The thing about class explosion isn't fear based at all. It's a phrase used in OO circles to indicate a tendency for a design decision to lead toward greater unmanageability as an application expands. My concern is born of the experience I've created for myself in my experiments with various architectures of having to jump from file to method() to file to method() to file to method() to file to method() to follow what's going on in just one "event" in an OO application.
When you employ more classes in your architecture, it gets harder to deal with the fragmentation that results as the application expands. That's the best argument for me personally for capable business objects, because I get increasingly confused by having to deal with a maze of objects and methods on an ongoing basis, even if I thought it was a good idea to use separate classes a few days or weeks ago and the way it all worked together was very clear in my mind when I conceived it.
Which is why I would be wary to put a form helper object in between a form and a business object, even if it seems like a good idea. It's my preference that a business object should be capable to both represent the thing it models in a valid state and perform the necessary translation between the typeless world of the internet and the typed world of our data storage mechanisms (and languages, if that's applicable). Not everyone will agree with me, and if my employment depends on the use of a more fragmented model, I will use a more fragmented model. ;-)
That, in fact, is exactly my argument, Nando. It's why I also don't like the Active Record Pattern much loved by Ruby on Rail fans.
@Nando,
I definitely know the pain of having to weave through 10 different files just to find out how something is processed! So yes, I agree that it is nice to condense things where possible.
What I meant with the class explosion was that the ratio was so small. To me, when I think of class explosion, I tend to think of the 1:5 ratio (gateway, service, bean, controller...etc). Or something where too many sub-classes need to be created. That's all. Not really a point worth exploring further.
@Hal,
Is your objection to Transfer more on principle? Or do you actually wind up with a significant number of business objects (if you use them) in your web applications that don't correspond with your database tables? Can you give examples relevant to web application design if this is the case?
Is the tradeoff toward architectural freedom worth the effort involved in coding your database interactions (and business objects) by hand?
I like the fact that Transfer provides a configuration file, so I decide which transfer objects I need and how to relate them. And I still have the freedom to design my model independently of the database. If you'd care to, I'd love to hear you make your case and expand on this topic.
I have come to a decision about viewing ColdFusion components as data types:
www.bennadel.com/index.cfm?dax=blog:1558.view