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 Scotch On The Rock (SOTR) 2010 (London) with: Andy Allan

ColdFusion Iterating Business Objects (IBOs) From The Ground-Up

By Ben Nadel on
Tags: ColdFusion

Peter Bell recently wrote an article for the ColdFusion Developer's Journal (CFDJ) on using Iterating Business Objects in ColdFusion. I have heard about these IBOs for a long time, so now that there was some source code to look at, I really dove right in. In order to learn more about a chunk of code, I often times try to build it from the ground up using my own formatting so that I can truly understand how all the pieces fit together without having to worry about the way it was written (from a stylistic standpoint).

As I was duplicating and modifying the code, there were some issues that began to come to light for me personally. Now, please don't take this as any sort of insult to Peter Bell's work. I think Peter's work is quite excellent. This is just a simple review by someone who has less experience.

BaseObject.cfc

For starters, I was not crazy about the name BaseObject.cfc. To me this is too general. This isn't just a base object; it provides base functionality for Iterating Business Objects. In my limited experience, I have come to think of these things as Abstract objects. So, to drive home the point that it has some functionality and is for IBOs, I have named my build AbstractIBO.cfc.

Access And Mutate

Ok, I simply didn't understand this one :) I kind of get the idea - we are hiding the implementation of the getting and setting from the Get() and Set() functions... but I just don't get why. My best guess is that this is some sort of Template Method Pattern where the extending, concrete classes can override the Access and Mutate functions. But, since I didn't understand it, I left it out. It can always be added back in later.

Number Of Records

It took me a while just to figure out why the iterating part of the IBO would work. To me it seemed like it would only ever iterate to N-1 records. Finally, I figured out that his private variable, VARIABLES.NumberOfRecords actually held the value N+1. While this is clever, it just seems misleading. I have actually removed this variable all together. In my mind, the Number of Records will always be related to the internal data structure and can be accessed as such. Of course, a NumberOfRecords would be a nice short hand, so I might put it back in later.

I do however have a public THIS.RecordCount (trying to keep with the ColdFusion query record set naming) that is equal to the internal data's record count. This way, there is no discrepancy between the public numbers and the private numbers. I think this goes a long way for helping others understand the code.

IsLast()

Peter loops over his IBO until IsLast() returns true. If you look at IsLast(), it returns true once the current iteration has gone past the last record. For me, this is just akward. If anything, this would be IsAfterLast(). Whether or not you like that naming convention, I think it is a bad idea to use IsLast() as that has a precedence in the Iterating world to mean "pointing AT the last record" not past it. Likewise, there is often times an IsFirst() which indicates pointing at the first record. In fact, the ColdFusion query objects have both an IsFirst() and IsLast() methods. To have one method mean something and related method mean something different is going to hurt the learning curve.

To keep naming conventions consistent, I have changed the IsLast() to mean is last. I have added a method called, IsRecord(). I am not sure I am sold on this name, but at least it has a bit more meaning to my brain. IsRecord() returns true if the current iteration is pointing towards a valid record. If this iteration goes past the record set, then this returns false. This just feels more natural.

Run Time Property Evaluation

When someone goes to Get() or Set() a property, Peter is checking to see if it is an allowed property - whether we are allowing "SELECT *" mentality or if it is in the explicit list of available properties. He then also checks to see if the property exists as a method or a record set property.

To me, this is not performance friendly. He is doing a ListFind(), and string evaluation, and an Evaluate() call. This is easy stuff, but I just felt that it could be faster. Instead of doing this kind of checking at Run Time of the Get() and Set() methods, I am doing this checking at the load time of the query, during the LoadQuery() method call.

As soon as the query is passed in, I build up the list of available properties based on the explicitly property list and the Get and Set functions of the concrete, extending object. I create a structural mapping that uses the property names a structure keys and the access type (Query column vs. Get/Set Method) as the value. Then, during the Get() and Set() calls, I can access this structure based on the property that was passed in.

Structures, as Hash Tables, have extremely fast look up. By storing the properties in this look-up structure, I can quite efficiently determine which properties are valid and how they should be accessed. This puts the bulk of the processing at query-load time (which is only run once) and allows for a very fast Get and Set actions (which have to happen on a N-times type basis).

Returning Empty String As Invalid

This is quite minor, but the BaseObject.cfc returns an empty string if the accessed property is not available. To me, this just seems a bit off. I have chosen to throw an error. This is minor detail and something that can probably be abstracted out to be overridden by the concrete, extending class.

LoadQuery()

Other than the fact that I calculate the available properties at this point, there was also some other code that I didn't understand. Peter was copying the query into an internal struct structure. I didn't really follow what was going on. It looks like maybe this was done to allow for a zero-length query. It may have also been done to have a completely separate data set (that would not be altered by outside pointers). Again, not really sure. In my build, I just Duplicate() the given query into the IBO. This is fairly fast action, but unlike what I think his might be doing, mine will break if you try to access a zero-record data set. I suppose his is more inline with how ColdFusion works.

Too Dynamic / Too Naming-Convention Dependent

As I was adding functionality to my IBO build, one thing that I realized is that it was too dependent on naming conventions. Namely, it checked to see if a private method started with "Get" or "Set" and that was it. This was too much flexibility for me. It was picking up methods that I didn't want it to pick up (ie. GetAccessHints() when using *). To work around this, I have added the "mapto" attribute to the CFFunction tag. Only functions that use this are available as a Getter or Setter method.

While this is not a valid attribute of the CFFunction tag, it is available as part of the function's Meta Data (GetMetaData( fnPointer )). This solution has several benefits in my mind.

  1. It explicitly points out which methods are for getting and setting attributes. Not only does this allow you to have other "Get" and "Set" prefixed methods that are NOT used for getting and setting properties, I find explicit declarations a bit more comforting. They help me sleep a bit better a night.
  2. It allows for differences in naming conventions between your database and your CFC methods. For example, I use the "_" in my database naming convention. A person's first name in the database would be "first_name". In order for this to be Get'ed as a function rather than a query column, I would have to have a CFC method named GetFirst_Name(). This violates my CFC naming convention which states that I cannot use "_" in CFC names or methods. What I want to do is have a CFC method named GetFirstName(). This however would NOT be picked up by the original IBO which relied only on naming conventions. The MapTo attribute allows me to have a GetFirstName() method and map it to the first_name property via mapto="first_name".

Now, On To The Code

So, that was my review of Peter Bell's Iterating Business Object. Again, please do not take this as a tear-down of his code. I think it is quite good and very innovative. I would never have come up with something like that in a million years. Pete knows how I feel. But anyway, let me show you what I came up with based on his code. This is flawed, but attempts to overcome the issues I have listed above.

AbstractIBO.cfc

  • <cfcomponent
  • displayname="AbstractIBO"
  • output="false"
  • hint="This handles the base functionality of an Iterating Business Object.">
  •  
  •  
  • <!--- Run the pseudo constructor to set up default data structures. --->
  • <cfscript>
  •  
  • // Set up the instance object in the private memory scope.
  • VARIABLES.Instance = StructNew();
  •  
  • // This is the index of the current iteration.
  • VARIABLES.Instance.IterationIndex = 1;
  •  
  • // These are the list of keys that can be "get"ed and "set"ed in some
  • // way. We are not going to define how they are accessed at this moment.
  • // That will be done once the underlying data structure is set. These will
  • // populate the Get/Set structures (that follow).
  • VARIABLES.Instance.GetAttributesList = "*";
  • VARIABLES.Instance.SetAttributesList = "*";
  •  
  • // This is a structure of keys that can be "get"ed and "set"ed. These
  • // will be populated based on the attributes list above.
  • VARIABLES.Instance.GetAttributes = StructNew();
  • VARIABLES.Instance.SetAttributes = StructNew();
  •  
  • // This is a structure that will alias the getter and setter methods.
  • // We are doing this so that we don't have to care about any method
  • // mapping.
  • VARIABLES.Instance.GetAttributeMethods = StructNew();
  • VARIABLES.Instance.SetAttributeMethods = StructNew();
  •  
  •  
  • // This is the underlying data object. I am initializing to an empty qyery.
  • // I can't believe this actually works, but at least it puts a query object
  • // as this variable value.
  • VARIABLES.Instance.RecordSet = QueryNew( "" );
  •  
  •  
  • // Set up the non-instance private data.
  •  
  • // These are the types of getter/setters that can be accessed/mutated.
  • // Right now, a value can either be accessed as a property or as a
  • // function of the underlying data structure. These values are meant to
  • // be constant and are therefore not part of the instance variable struct.
  • VARIABLES.KeyTypes = StructNew();
  •  
  • // This defines a key as being accessed as a method.
  • VARIABLES.KeyTypes.METHOD = 1;
  •  
  • // This defines a key as being accessed as a property.
  • VARIABLES.KeyTypes.PROPERTY = 2;
  •  
  •  
  • // Set up the default public data.
  •  
  • THIS.RecordCount = 0;
  • THIS.CurrentRow = 0;
  •  
  • </cfscript>
  •  
  •  
  • <cffunction name="Init" access="public" returntype="AbstractIBO" output="false"
  • hint="Returns an initialized Abstract IBO instance.">
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction name="First" access="public" returntype="void" output="false"
  • hint="Resets the IBO cursor to point to the first record in the record set.">
  •  
  • <!--- Set the index. --->
  • <cfset VARIABLES.Instance.IterationIndex = 1 />
  •  
  • <!--- Set the public data. --->
  • <cfset THIS.CurrentRow = 1 />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction name="Get" access="public" returntype="any" output="false"
  • hint="Gets the value at the given key. Throws an error if the key is not available.">
  •  
  • <!--- Define arguments. --->
  • <cfargument name="Key" type="string" required="true" />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  • <!--- Check to see if the key is available. --->
  • <cfif StructKeyExists( VARIABLES.Instance.GetAttributes, ARGUMENTS.Key )>
  •  
  • <!--- This key is valid. Check to see how it is being accessed. --->
  • <cfif (VARIABLES.Instance.GetAttributes[ ARGUMENTS.Key ] EQ VARIABLES.KeyTypes.PROPERTY)>
  •  
  • <!--- Return the property straight out of the record set. --->
  • <cfreturn VARIABLES.Instance.RecordSet[ ARGUMENTS.Key ][ VARIABLES.Instance.IterationIndex ] />
  •  
  • <cfelse>
  •  
  • <!---
  • The key is being accessed as a method. Get a pointer to the method.
  • We need to get the interim method since ColdFusion cannot handle the
  • parsing of an array notation followed by a method call.
  • --->
  • <cfset LOCAL.Method = VARIABLES.Instance.GetAttributeMethods[ ARGUMENTS.Key ] />
  •  
  • <!--- Return the value of the get method. --->
  • <cfreturn LOCAL.Method() />
  •  
  • </cfif>
  •  
  • <cfelse>
  •  
  • <!--- The key is not available for getting. Throw an error. --->
  • <cfthrow
  • message="You have attempted to access an invalid key."
  • type="IBO.InvalidGetKey"
  • detail="The key you are trying to get, which is currently #UCase( ARGUMENTS.Key )#, is not available in this IBO."
  • />
  •  
  • </cfif>
  • </cffunction>
  •  
  •  
  • <cffunction name="GetAccessHints" access="public" returntype="struct" output="false"
  • hint="This is meant for debugging so you can see how the properties are being accessed.">
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  • <!--- Create a structure to return both the getter and setter info. --->
  • <cfset LOCAL.Hints = StructNew() />
  •  
  • <!--- Set the keys so that the access methods make sense. --->
  • <cfset LOCAL.Hints.Keys = Duplicate( VARIABLES.KeyTypes ) />
  •  
  • <!--- Set the getters and setters. --->
  • <cfset LOCAL.Hints.Get = Duplicate( VARIABLES.Instance.GetAttributes ) />
  • <cfset LOCAL.Hints.Set = Duplicate( VARIABLES.Instance.SetAttributes ) />
  •  
  • <!--- Return the hints. --->
  • <cfreturn LOCAL.Hints />
  • </cffunction>
  •  
  •  
  • <cffunction name="GetPropertyList" access="public" returntype="string" output="false"
  • hint="Returns the property list.">
  •  
  • <cfreturn VARIABLES.Instance.GetAttributesList />
  • </cffunction>
  •  
  •  
  • <cffunction name="IsFirst" access="public" returntype="boolean" output="false"
  • hint="Determines if the IBO is pointing at the first record.">
  •  
  • <cfreturn (VARIABLES.Instance.IterationIndex EQ 1) />
  • </cffunction>
  •  
  •  
  • <cffunction name="IsLast" access="public" returntype="boolean" output="false"
  • hint="Determines if the IBO is pointing at the last record.">
  •  
  • <cfreturn (VARIABLES.Instance.IterationIndex EQ VARIABLES.Instance.RecordSet.RecordCount) />
  • </cffunction>
  •  
  •  
  • <cffunction name="IsRecord" access="public" returntype="boolean" output="false"
  • hint="Determines if the IBO is pointing at a valid record. If the IBO goes past the last index of the current record set, this will return false.">
  •  
  • <cfreturn (VARIABLES.Instance.IterationIndex LTE VARIABLES.Instance.RecordSet.RecordCount) />
  • </cffunction>
  •  
  •  
  • <cffunction name="LoadQuery" access="public" returntype="any" output="false"
  • hint="This loads the given query data into the IBO.">
  •  
  • <!--- Define arguments. --->
  • <cfargument name="RecordSet" type="query" required="true" />
  •  
  • <cfscript>
  •  
  • // Define the local scope.
  • var LOCAL = StructNew();
  •  
  • // Duplicate the query when storing it in the IBO. I am not sure that
  • // I completely agree with this, but it seems that this is (in part)
  • // what the original code is doing. We are doing this so that even if
  • // the query is altered after it is sent it, it does not affect the
  • // underlying data of the IBO.
  • VARIABLES.Instance.RecordSet = Duplicate( ARGUMENTS.RecordSet );
  •  
  • // Now that we have the data stored in our IBO, we can populate the
  • // structures of getters and setters. If the list is "*" then we
  • // are going to copy the record set's column list over into the list
  • // of getters and setters. Otherwise, we will keep it as is.
  •  
  • // Set local flags for select all.
  • LOCAL.IsGetAll = VARIABLES.Instance.GetAttributesList.Matches( "\*" );
  • LOCAL.IsSetAll = VARIABLES.Instance.SetAttributesList.Matches( "\*" );
  •  
  • // Check to see if we are getting all properties.
  • if (LOCAL.IsGetAll){
  •  
  • // The Get attributes list will be the record set's column list.
  • VARIABLES.Instance.GetAttributesList = VARIABLES.Instance.RecordSet.ColumnList;
  •  
  • }
  •  
  • // Check to see if we are setting all properties.
  • if (LOCAL.IsSetAll){
  •  
  • // The Set attributes list will be the record set's column list.
  • VARIABLES.Instance.SetAttributesList = VARIABLES.Instance.RecordSet.ColumnList;
  •  
  • }
  •  
  •  
  • // At this point, we have the get and set attributes list which have
  • // been populated either by the record set OR hard coded by the concrete
  • // extending IBO. Let's store these keys into the get and set attributes
  • // structures for faster lookup and editing.
  •  
  • // Loop over get property list.
  • for (
  • LOCAL.PropertyIndex = 1 ;
  • LOCAL.PropertyIndex LTE ListLen( VARIABLES.Instance.GetAttributesList ) ;
  • LOCAL.PropertyIndex = (LOCAL.PropertyIndex + 1)
  • ){
  •  
  • // Get the current property.
  • LOCAL.Property = ListGetAt( VARIABLES.Instance.GetAttributesList, LOCAL.PropertyIndex );
  •  
  • // Set the property to a default value.
  • VARIABLES.Instance.GetAttributes[ LOCAL.Property ] = 0;
  • }
  •  
  • // Loop over set property list.
  • for (
  • LOCAL.PropertyIndex = 1 ;
  • LOCAL.PropertyIndex LTE ListLen( VARIABLES.Instance.SetAttributesList ) ;
  • LOCAL.PropertyIndex = (LOCAL.PropertyIndex + 1)
  • ){
  •  
  • // Get the current property.
  • LOCAL.Property = ListGetAt( VARIABLES.Instance.SetAttributesList, LOCAL.PropertyIndex );
  •  
  • // Set the property to a default value.
  • VARIABLES.Instance.SetAttributes[ LOCAL.Property ] = 0;
  • }
  •  
  •  
  • // Now, let's collect the IBO methods that have been mapped to properties.
  • // This is done to allow for differences in naming conventions between the
  • // database (which results in the record set) and the CFC methods. For
  • // instance, the database might have "first_name", but for naming convention's
  • // sake, we can't have a method named "GetFist_Name". To accomodate this,
  • // we are forcing the methods to use the "mapto" attribute. Method's without
  • // this attribute (for both GET and SET) will be ignored.
  •  
  • // Loop over the methods to check for anything that begins with "get" or "set".
  • for (LOCAL.CFCProperty in VARIABLES){
  •  
  • // Check to see if this is a user-defined function. We only care
  • // about things that can be called as functions.
  • if (
  • IsCustomFunction( VARIABLES[ LOCAL.CFCProperty ] ) AND
  • StructKeyExists( GetMetaData( VARIABLES[ LOCAL.CFCProperty ] ), "MapTo" ) AND
  • (
  • (Left( LOCAL.CFCProperty, 3 ) EQ "GET") OR
  • (Left( LOCAL.CFCProperty, 3 ) EQ "SET")
  • )){
  •  
  • // Get the mapto attribute value.
  • LOCAL.MapTo = GetMetaData( VARIABLES[ LOCAL.CFCProperty ] ).MapTo;
  •  
  • // Check to see if this is a get method and that it is in the defined
  • // list of "get"able properties.
  • if (
  • (Left( LOCAL.CFCProperty, 3 ) EQ "GET") AND
  • (
  • LOCAL.IsGetAll OR
  • StructKeyExists( VARIABLES.Instance.GetAttributes, GetMetaData( VARIABLES[ LOCAL.CFCProperty ] ).MapTo )
  • )){
  •  
  • // Flag this property as being accessed as a method.
  • VARIABLES.Instance.GetAttributes[ LOCAL.MapTo ] = VARIABLES.KeyTypes.METHOD;
  •  
  • // Set the method alias.
  • VARIABLES.Instance.GetAttributeMethods[ LOCAL.MapTo ] = VARIABLES[ LOCAL.CFCProperty ];
  •  
  • // Check to see if this is a set method and that it is in the defined
  • // list of "set"able properties.
  • } else if (
  • (Left( LOCAL.CFCProperty, 3 ) EQ "SET") AND
  • (
  • LOCAL.IsSetAll OR
  • StructKeyExists( VARIABLES.Instance.SetAttributes, GetMetaData( VARIABLES[ LOCAL.CFCProperty ] ).MapTo )
  • )){
  •  
  • // Flag this property as being accessed as a method.
  • VARIABLES.Instance.SetAttributes[ LOCAL.MapTo ] = VARIABLES.KeyTypes.METHOD;
  •  
  • // Set the method alias.
  • VARIABLES.Instance.SetAttributeMethods[ LOCAL.MapTo ] = VARIABLES[ LOCAL.CFCProperty ];
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  •  
  • // Convert the record set column list to a structure for faster access.
  • LOCAL.Columns = StructNew();
  •  
  • // Loop over columns and add keys to column struct.
  • for (
  • LOCAL.ColumnIndex = 1 ;
  • LOCAL.ColumnIndex LTE ListLen( VARIABLES.Instance.RecordSet.ColumnList ) ;
  • LOCAL.ColumnIndex = (LOCAL.ColumnIndex + 1)
  • ){
  •  
  • // Add to struct. The value here is not important.
  • LOCAL.Columns[ ListGetAt( VARIABLES.Instance.RecordSet.ColumnList, LOCAL.ColumnIndex ) ] = 0;
  • }
  •  
  •  
  • // At this point, we should already have all the mapped methods accounted for.
  • // Now, we have to figure out which of the properties will be accessed as a
  • // property (and which ones are not valid in any access form). To get this,
  • // let's loop over the get and set properties and for each property that is
  • // not being accessed as a method, check to see if there is an equivalent
  • // record set column.
  •  
  • // Loop over get attributes.
  • for (LOCAL.Property in VARIABLES.Instance.GetAttributes){
  •  
  • // Check to see if this attribute is zero. This would indicate a
  • // property that has not been validated yet (and is just based on
  • // the list of properties).
  • if (NOT VARIABLES.Instance.GetAttributes[ LOCAL.Property ]){
  •  
  • // This property has not been accounted for. Check for a
  • // matching record set column.
  • if (StructKeyExists( LOCAL.Columns, LOCAL.Property )){
  •  
  • // There is a matching record set column. Set this property as
  • // being accessed as a standard property.
  • VARIABLES.Instance.GetAttributes[ LOCAL.Property ] = VARIABLES.KeyTypes.PROPERTY;
  •  
  • } else {
  •  
  • // This property has not been accounted for by either the mapped
  • // methods or the record set columns. Delete this property from
  • // the get attributes.
  • StructDelete( VARIABLES.Instance.GetAttributes, LOCAL.Property );
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  •  
  • // Loop over set attributes.
  • for (LOCAL.Property in VARIABLES.Instance.SetAttributes){
  •  
  • // Check to see if this attribute is zero. This would indicate a
  • // property that has not been validated yet (and is just based on
  • // the list of properties).
  • if (NOT VARIABLES.Instance.SetAttributes[ LOCAL.Property ]){
  •  
  • // This property has not been accounted for. Check for a
  • // matching record set column.
  • if (StructKeyExists( LOCAL.Columns, LOCAL.Property )){
  •  
  • // There is a matching record set column. Set this property as
  • // being accessed as a standard property.
  • VARIABLES.Instance.SetAttributes[ LOCAL.Property ] = VARIABLES.KeyTypes.PROPERTY;
  •  
  • } else {
  •  
  • // This property has not been accounted for by either the mapped
  • // methods or the record set columns. Delete this property from
  • // the Set attributes.
  • StructDelete( VARIABLES.Instance.SetAttributes, LOCAL.Property );
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  •  
  • // At this point, we have set up the Get and Set attributes based on the
  • // mapped methods and the record set column list. There might be some
  • // differences between the hard coded set/get lists and the available
  • // Set and Get attributes. Not sure how to handle that at the moment. I
  • // am going to leave it as-is for now. However, we could certainly set
  • // the get and let list based on the keys of the attribute structrures:
  • //
  • // For example:
  • // VARIABLES.Instance.GetAttributesList = StructKeyList( VARIABLES.Instance.GetAttributes );
  • // VARIABLES.Instance.SetAttributesList = StructKeyList( VARIABLES.Instance.SetAttributes );
  •  
  • // Set public data.
  • THIS.RecordCount = VARIABLES.Instance.RecordSet.RecordCount;
  • THIS.CurrentRow = 1;
  •  
  •  
  • // Now that we have populated the IBO's getter/setter definitions, return
  • // the This reference. We are doing this so that the LoadQuery() method can
  • // be chained with the IBO's constructor.
  • return( THIS );
  •  
  • </cfscript>
  • </cffunction>
  •  
  •  
  • <cffunction name="Next" access="public" returntype="boolean" output="false"
  • hint="Increments the IBO cursor to point to the next record in the record set. Returns true if the new iteration is pointing to a valid record.">
  •  
  • <cfscript>
  •  
  • // Increment the iteration index to point to the next record. At this
  • // point, we don't care about any sort of validation. That is going
  • // to be taken care of in the next step.
  • VARIABLES.Instance.IterationIndex = (VARIABLES.Instance.IterationIndex + 1);
  •  
  • // Point the public row to the iteration index.
  • THIS.CurrentRow = VARIABLES.Instance.IterationIndex;
  •  
  • // Check to see if the new index points to a valid row. If it does, return true
  • // otherwise, return false.
  • return( VARIABLES.Instance.IterationIndex LTE VARIABLES.Instance.RecordSet.RecordCount );
  •  
  • </cfscript>
  • </cffunction>
  •  
  •  
  • <cffunction name="Set" access="public" returntype="void" output="false"
  • hint="Sets the value at the given key. Throws an error if the key is not available.">
  •  
  • <!--- Define arguments. --->
  • <cfargument name="Key" type="string" required="true" />
  • <cfargument name="Value" type="any" required="true" />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  • <!--- Check to see if the key is available. --->
  • <cfif StructKeyExists( VARIABLES.Instance.SetAttributes, ARGUMENTS.Key )>
  •  
  • <!--- This key is valid. Check to see how it is being accessed. --->
  • <cfif (VARIABLES.Instance.SetAttributes[ ARGUMENTS.Key ] EQ VARIABLES.KeyTypes.PROPERTY)>
  •  
  • <!--- Set the property directly in the record set. --->
  • <cfset VARIABLES.Instance.RecordSet[ ARGUMENTS.Key ][ VARIABLES.Instance.IterationIndex ] = ARGUMENTS.Value />
  •  
  • <cfelse>
  •  
  • <!---
  • The key is being accessed as a method. Get a pointer to the method.
  • We need to get the interim method since ColdFusion cannot handle the
  • parsing of an array notation followed by a method call. Furthermore,
  • we don't want to use CFInvoke as CFInvoke requires the naming of the
  • arguments. By creating a method pointer, we can use positional argument
  • mapping, not mappings by name. This makes for a more flexible interface.
  • --->
  • <cfset LOCAL.Method = VARIABLES.Instance.SetAttributeMethods[ ARGUMENTS.Key ] />
  •  
  • <!--- Call the set method. --->
  • <cfset LOCAL.Method( ARGUMENTS.Value ) />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • At this point we have either set the value as a property or by
  • method. Eitherway, return out.
  • --->
  • <cfreturn />
  •  
  • <cfelse>
  •  
  • <!--- The key is not available for setting. Throw an error. --->
  • <cfthrow
  • message="You have attempted to access an invalid key."
  • type="IBO.InvalidSetKey"
  • detail="The key you are trying to set, which is currently #UCase( ARGUMENTS.Key )#, is not available in this IBO."
  • />
  •  
  • </cfif>
  • </cffunction>
  •  
  • </cfcomponent>

GirlIBO.cfc

The GirlIBO.cfc ColdFusion component is a concrete IBO that extends the AbstractIBO.cfc. It provides some of its own Getter and Setter methods. These override the getting and setting of straight up query columns.

  • <cfcomponent
  • displayname="GirlIBO"
  • extends="AbstractIBO"
  • output="false"
  • hint="Extends the abstract IBO interface, specialized for girls.">
  •  
  •  
  • <!--- Run the pseudo constructor to set up default data structures. --->
  • <cfscript>
  •  
  • // These are the list of keys that can be "get"ed and "set"ed in some
  • // way. We are not going to define how they are accessed at this moment.
  • // That will be done once the underlying data structure is set. These will
  • // populate the Get/Set structures (that follow).
  • // VARIABLES.Instance.GetAttributesList = "id,name,sexyness_factor";
  • // VARIABLES.Instance.SetAttributesList = "";
  •  
  • </cfscript>
  •  
  •  
  • <cffunction name="GetFullName" access="private" returntype="string" output="false"
  • mapto="full_name"
  • hint="Gets the full name of the girl based on the name and sexyness.">
  •  
  • <!--- Check to see what the sexyness factor is when we determine the girl's full name. --->
  • <cfif (VARIABLES.Instance.RecordSet[ "sexyness_factor" ][ VARIABLES.Instance.IterationIndex ] GTE 9)>
  •  
  • <!--- This a really hot girl. --->
  • <cfreturn (
  • "Crazy Hot " &
  • VARIABLES.Instance.RecordSet[ "name" ][ VARIABLES.Instance.IterationIndex ]
  • ) />
  •  
  • <cfelseif (VARIABLES.Instance.RecordSet[ "sexyness_factor" ][ VARIABLES.Instance.IterationIndex ] GTE 8)>
  •  
  • <!--- This a sexy girl. --->
  • <cfreturn (
  • "Sexy " &
  • VARIABLES.Instance.RecordSet[ "name" ][ VARIABLES.Instance.IterationIndex ]
  • ) />
  •  
  • <cfelse>
  •  
  • <!--- This a really an average girl. --->
  • <cfreturn VARIABLES.Instance.RecordSet[ "name" ][ VARIABLES.Instance.IterationIndex ] />
  •  
  • </cfif>
  • </cffunction>
  •  
  •  
  • <cffunction name="GetSexynessFactor" access="private" returntype="numeric" output="false"
  • mapto="sexyness_factor"
  • hint="Gets the sexyness as an integer (fixes the value).">
  •  
  • <cfreturn Fix( VARIABLES.Instance.RecordSet[ "sexyness_factor" ][ VARIABLES.Instance.IterationIndex ] ) />
  • </cffunction>
  •  
  •  
  • <cffunction name="SetName" access="public" returntype="void" output="false"
  • mapto="name"
  • hint="Sets the name as all upper case.">
  •  
  • <!--- Define arguments. --->
  • <cfargument name="Value" type="string" required="true" />
  •  
  • <!--- Set the value and convert to upper case. --->
  • <cfset VARIABLES.Instance.RecordSet[ "name" ][ VARIABLES.Instance.IterationIndex ] = JavaCast( "string", ARGUMENTS.Value.ToUpperCase() ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

Testing The Code

  • <!--- Build a test query. --->
  • <cfset qGirl = QueryNew(
  • "id, name, sexyness_factor",
  • "CF_SQL_INTEGER, CF_SQL_VARCHAR, CF_SQL_DECIMAL"
  • ) />
  •  
  •  
  • <!--- Add rows to the test query. --->
  • <cfset QueryAddRow( qGirl, 3 ) />
  •  
  • <!--- Set the row data for the query. --->
  • <cfset qGirl[ "id" ][ 1 ] = JavaCast( "int", 1 ) />
  • <cfset qGirl[ "name" ][ 1 ] = JavaCast( "string", "Sarah" ) />
  • <cfset qGirl[ "sexyness_factor" ][ 1 ] = JavaCast( "float", 10.0 ) />
  •  
  • <cfset qGirl[ "id" ][ 2 ] = JavaCast( "int", 2 ) />
  • <cfset qGirl[ "name" ][ 2 ] = JavaCast( "string", "Libby" ) />
  • <cfset qGirl[ "sexyness_factor" ][ 2 ] = JavaCast( "float", 8.5 ) />
  •  
  • <cfset qGirl[ "id" ][ 3 ] = JavaCast( "int", 3 ) />
  • <cfset qGirl[ "name" ][ 3 ] = JavaCast( "string", "Alex" ) />
  • <cfset qGirl[ "sexyness_factor" ][ 3 ] = JavaCast( "float", 7.5 ) />
  •  
  •  
  • <!---
  • Create the IBO and load in the query. Notice that I can
  • chain these two actions together, which the original
  • IBO could not. Just a neat little feature.
  • --->
  • <cfset objIBO = CreateObject(
  • "component",
  • "GirlIBO"
  • ).Init().LoadQuery( qGirl ) />
  •  
  •  
  • <!---
  • Loop over the IBO. You might want to run a First() call
  • before iterating, but for this demo, that is not
  • required.
  • --->
  • <cfloop condition="objIBO.IsRecord()">
  •  
  • <!--- Set the name just to test the set as fn. --->
  • <cfset objIBO.Set(
  • "name",
  • objIBO.Get( "name" )
  • ) />
  •  
  • <p>
  • #objIBO.Get( "full_name" )# ::
  • #objIBO.Get( "sexyness_factor" )#
  • </p>
  •  
  • <!--- Go to the next record. --->
  • <cfset objIBO.Next() />
  • </cfloop>

One of the things that I don't like about the IBO (or iterators in general) is that there is generally more code involved with the performed actions. In this case, we have to move the IBO to point to the first record (if we have repeat iterations) and for every iteration, we have to move the IBO to point to the next record (via the Next()) method. Once we have the IBO interface nailed down, to get around this extra code, we can then create a ColdFusion custom tag that will loop over the IBO for us. This will do the same exact thing that we did manually, it will just do the record validation and the Next() commands behind the scenes:

  • <!--- Kill extra output. --->
  • <cfsilent>
  •  
  • <!--- Param the tag attributes. --->
  •  
  • <!--- This is the IBO object that we are using to iterate. --->
  • <cfparam
  • name="ATTRIBUTES.IBO"
  • type="any"
  • />
  •  
  •  
  • <!---
  • Check to see which mode of the tag we are executing. This
  • tag is being used to loop back over itself using the LOOP
  • value of the CFExit tag. When that happens, the Start mode
  • of the tag only gets fired once. Every other iteration fired
  • the End mode of the tag. Therefore, most of our actions are
  • done in the End mode.
  • --->
  • <cfswitch expression="#THISTAG.ExecutionMode#">
  •  
  • <cfcase value="START">
  •  
  • <!---
  • Move the IBO to it's first record. While this is not always
  • necessary, if this IBO has already been used for iteration,
  • then this is required.
  • --->
  • <cfset ATTRIBUTES.IBO.First() />
  •  
  • </cfcase>
  •  
  • <cfcase value="END">
  •  
  • <!--- Move on to next row iteration. --->
  • <cfset ATTRIBUTES.IBO.Next() />
  •  
  • <!--- Check to see if we have a record to point to. --->
  • <cfif ATTRIBUTES.IBO.IsRecord()>
  •  
  • <!---
  • We are still at a valid record. Allow this tag to
  • execute again. Loop to next record.
  • --->
  • <cfexit method="LOOP" />
  •  
  • <cfelse>
  •  
  • <!---
  • The Next() command we just performed put out IBO outside
  • the range of valid records. Do not let this tag execute
  • again. Exit out of the tag.
  • --->
  • <cfexit method="EXITTAG" />
  •  
  • </cfif>
  •  
  • </cfcase>
  •  
  • </cfswitch>
  •  
  • </cfsilent>

And then, once we have that ColdFusion custom tag down, we can use it to loop in a fashion similar to the CFLoop tag:

  • <cfmodule
  • template="./iboloop.cfm"
  • ibo="#objIBO#">
  •  
  • <p>
  • #objIBO.Get( "full_name" )# ::
  • #objIBO.Get( "sexyness_factor" )#
  • </p>
  •  
  • </cfmodule>

So that's my informal review of Peter Bell's ColdFusion Iterating Business Object. I think Peter is on to something very cool here. I am 100% sold yet, but I am never completely sold on stuff that I have never used before. I like things to be tested over time. I am not convinced that my modifications to the IBO are any better or worse than the original product; I like some of my conventions better, but a lot of that is personal, not functional. I know that Peter does use this sort of stuff in Production so it must be pretty useful. I wait with baited breathe for him to post of some new code.



Reader Comments

Hey Ben,

Great review - many thanks for taking the time to do this!!! Let me clarify some of the design decisions:

BaseObject.cfc - You're right - it isn't a base object. I personally now call it BaseIBO.cfc as that it what it is - a base IBO. It isn't actually truly abstract as I will show you can actually instantiate the base class in place of the extending class if you don't have any custom getters or setters. Look to my blog next week for ideas on that! BaseBusinessObject.cfc is something I also toyed with, but I'm cool with BaseIBO as I think that is most descriptive. If you were never going to initialize it directly, I think AbstractIBO would also work.

Access and Mutate - Lets say you have a CUSTOM method called getPrice(). The logic is that if there is a sale price, return that, if not, return the price. How do you write this? If variables.get("SalePrice") ReturnValue = variables.get("SalePrice") else ReturnValue = Price. But how do you call price? If you know how the price is stored you could call variables[variables.RecordCount].Price, but now every custom method knows the strructure of that struct and I may want to change how I store that. But I can't just call get("Price") or I'll be in an infinite recursive loop. So I need what I used to call the "hard get" method to NOT check for whether there is a custom method but just to go get the value. That is why I have access() and so the else statement is now "else ReturnValue = Variables.access("Price")". Try to implement price and sale price without access and mutate methods and you'll run into the same problem.

Number of records - A hack I'm not proud of and am fixing today. The best solution is simply removing the IsLast() method. If you'll note you will see that Next() returns a boolean, so that should be all that is required to implement the iterator. Let me play with this and see if I can get it working as I want it to.

Run time property evaluation - You are assuming that all of the loaded properties are available for getting. This is NOT A SAFE ASSUMPTION. Lets say I have a dateofbirth that I load into the object only to expose a getAge() method. I may not want people to be able to get the date of birth even though it exists. That is the key to information hiding - the object may contain properties that should not be publically accessible. Nothing wrong with using a struct instead of a list (but I'm going to treat that as premature optimization until I see this being a performance bottleneck), but it MUST be built based on the list of gettable and/or settable properties (these can be built up from the loadquery IF they are set to * or all which is NOT generally a best practice) otherwise you lose all of the information hiding benefits of the IBO.

Great review - thanks! Let me know if any of the descriptions above don't make sense.

Reply to this Comment

Peter,

Thanks for taking a look at the stuff I wrote down.

As far as "Abstract" vs. "Base", this is just my inexperience as a OOP programmer. I am not set on anything in particular. I just thought Abstract objects were meant to be extended. I understand that this IBO doesn't have to be extended, that it can be instantiated on its own. In my example, I extended it purely to test the Get/Set overrides.

For the access and mutate stuff, I am mostly with you on that. I don't quite understand 100%, but I am sure if I go over it a few more times, it will makes sense.

As for the final point about run time property evaluation, I am NOT assuming that all loaded properties are available for getting and setting. Well, not totally. If someone uses the select "" method, then my "property list" consists of all columns of the query PLUS any methods that have "mapto" attributes. If select "" is not used, then I use only the overlapped columns in the query set... however, true, I DO add all methods that use the "mapto" attribute regardless of whether they are in the property list.

I think my decision to do that is that currently the select list is hard coded. So, my feeling is that if you include the overridden Get/Set method, then it SHOULD be used even if your select list does not have it.

What I think should really happen is that the INIT() method should take the Get and Set property lists and then I would feel more safe about excluding functions that are available in the extending CFC.

Reply to this Comment

Sorry for the bold text. I have to take that stuff out. It's bothering me. I think I will just allow people to use some basic HTML tags.

Reply to this Comment

Hey Ben,

Yeah. Abstract means that something should never be instantiated. In fact, a convention I'm using now is that if I create an abstract class, I usually don't give it an init() method to make it really clear it shouldn't be instantiated.

We're close on getter and setter list, but here is the thing. Sometimes you want to have a column in your recordset and to NOT allow people to access that column. I might select FirstName,LastName,DateofBirth from the db and have a custom getAge() which uses DoB to display age, but my gettableattributelist might be FirstName,LastName,Age - i.e. even though DateofBirth is in the returned recordset, people are not allowed to view it. That is key to the information hiding. That was one of my requirements for the IBO, so you could turn that list into a struct on init() if you wanted to, but you can't just take that from the db column list plus any custom methods as the whole point is to NOT share all of the columns from the database query to keep your objects shy (you don't need to know my DoB - just my age - I as an object just happen to need DoB as a private variable to be able to calculate my age but I don't want you knowing about that).

Make sense?

Reply to this Comment

Pete,

That's a good idea about the Abstract not having an Init(). I think a related method that I have seen is to have an Init that throws a custom error saying something like "This method must be overridden by an extending class." Nice convention though... I shall make it my own :)

As far as the getters and setters, I think we are just miscommunicating (I think). For your scendario, my select list would have "FirstName,LastName" and then have the mapto attribute of the GetAge() method be "age". The select list would exclude the "DOB" column from the query.

The only time I use all columns from the query is if "*" is used. Otherwise, you cannot get or set any query column that is not in the GetAttributesList or SetAttributesList.

I think we are saying pretty much the same thing. The main difference is that I use a "mapto" to associate methods, not the set/get lists. But in hind sight I see how that could be done.

Reply to this Comment

This company stock (ROKE) is set to take off. Worldwide client base in the mobile communications space. See the details at any clarification contact at
icoft123@gmail.com

Reply to this Comment

Hi
I used the iterative business object in a few oo websites with some good results. But i ran into an issue that is obvious in hindsight.

You cant cache these objects and share them in multiple requests, since the iterator needs to be specific to the running thread i.e. 2 incoming requests come in, the first gets the object and starts iterating, the second comes in moments later, gets the SAME object and resets the iterator thus messing it up for the first thread.

Has anyone come across a way around this? I.e. a thread safe way of using these objects. Its nice to have built an ibo from potentially a long query etc but you would like to cache them (good for memory use too to resuse objects). You could just cache the query object and or use cached queries but its not as clean as just sticking an object in a cache to use at will.

Reply to this Comment

@Adrian,

What I think you'll want to do is cache the query that populates it, not the CFC itself. Creating the single CFC shouldn't be a huge hit, but caching the data from page to page will be very beneficial.

Reply to this Comment

Hi Ben
Thank for the reply.
Caching things like the underlying queries is an option but its messy especially when you have seperation of layers i.e. the caching only exists on the web layer and not the service layer or when you build up an ibo object from disparate sources, multiple related queries that individually you wouldnt want to cache.

What we decided to do was build an helper object that captures the payload of the ibo and has a package level get and setPayload methods and added similar public methods on the ibo abstract cfc. This at least gives us encapsulation. We could even add a type property to prevent payload objects being added to incorrect ibos e.g. a payload object of type plants cant be added to ibo of type animals etc

Means building more objects that wouldve been nice to avoid and is a tad messy but hopefully it wont be too much of an overhead.

Reply to this Comment

A more important aspect of the IBO is to have it be an abstract class where other abstract classes can inherit from it. For example, I'm considering creating an abstract class (CFC) called Assignment. Its child, real classes are PermanentAssignment and TemporaryAssignment.

I figure it's more appropriate to let these child classes inherit from an abstract class because they share common method stubs (e.g., getStartDate(), getEndDate()) but have their own way implementing these methods.

Or I'm considering making this simple and just keep my IBO as a non-abstract class and have it sub-classed by the Assignment class (with PermanentAssignment and TemporaryAssignment sub-classing the Assignment class), but I don't ever see the need in intantiating a generic Assignment, so why make it non-abstract? Any thoughts?

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.