By Peter Bell

Rebuilding my Base Object

As you might have guessed from my last few postings, I'm doing a ground up "clean up" of all of my LightBase code. The starting point is to give my base object a quick going over . . .

There are quite a few functions in the BaseObject in LightBase that is requires for all of the magic to work, but rather than putting them all into the new version of the code immediately, I'm just going to drop in methods as they are required. To start with I need to be able to initialize, get, set and load properties as well as to have a dump() method (which I use all the time for ad-hoc debugging of my cfscript blocks). I'm also going to add an optional onMissingMethod() implementation so I have the option of calling getWhatever() instead of get("Whatever") to make the system a little more flexible. It'll just be a very simple facade to my generic getters and setters, and later to my "call()" method that allows me to describe methods using paraemeterized method calls which allows me to get rid of most of my Service class, DAO and Controller code.

I've already talked about how the getters and setters should work, and I think the dump() method speaks for itself - providing a cfdump()/cfabort() combo that can be accessed from a cfscript block.

The init method is pretty straightforward - just taking the optional GettablePropertyList and SettablePropertyList (for quick and easy testing both can be defaulted to * to allow anything to be get/set but you probably wouldn't want to develop that way for any period of time except for special cases). If they contain lists, it then creates a struct to make the lookup faster than looking up against a list (I'm not convinced the optimization is required yet, but enough people seemed to be bothered by looking up against a list that I decided just to add the extra few lines of code).

One other method is the load() method. This allows for the quick and easy loading of a bean using a struct. This is really handy as it allows you to tell a Input bean to load all of the elements in a form struct or a URL struct by just writing Input.load(form);. The prefix parameter is used for cases where a structure uses string prefixes. For example if you only want to load Billing information into the bean and you have a BillingAddress1 key, that would get loaded into the bean as Address1, but ShippingAddress1 wouldn't get loaded into the bean at all. I've found this to be really useful in practice.

I also have an asStruct() method which I've found useful for returning all of the gettable properties of an object as a structure just by calling object.asStruct().

Code below. As always, input appreciated. Below is rough first draft code which works but will probably fail for certain edge cases. Next step is to write unit tests (expect a posting tomorrow) and then hopefully I can get into the flow of writing the tests first before the end of the week!

[update] Been doing some more thinking about the asStruct() method as there are a few different intents which could work differently. I'm going to think this through and post somethign up separately. Consider the asStruct() below a placeholder.

<cfcomponent displayname="Base Object" hint="I'm the base object that all objects extend." output="false">

<cffunction name="init" output="false" returntype="any" access="public">
   <cfargument name="GettablePropertyList" type="string" required="false" default="*" hint="A comma delimited list of instance properties that should be gettable. Defaults to * for all.">
   <cfargument name="SettablePropertyList" type="string" required="false" default="*" hint="A comma delimited list of instance properties that should be settable. Defaults to * for all.">
   <cfscript>
      var i = 0;
      // Create structs for more performant processing of gettable/settable property lists       variables.GettablePropertyList = arguments.GettablePropertyList;
      GettablePropertyStruct = StructNew();
      If (len(GettablePropertyList) GT 1) {
         For (i = 1; i lte listlen(GettablePropertyList); i = i + 1) {
            PropertyName = ListGetAt(GettablePropertyList, i);
            GettablePropertyStruct[PropertyName] = 1;
         };
      };
      variables.SettablePropertyList = arguments.SettablePropertyList;
      SettablePropertyStruct = StructNew();
      If (len(SettablePropertyList) GT 1) {
         For (i = 1; i lte listlen(SettablePropertyList); i = i + 1) {
            PropertyName = ListGetAt(SettablePropertyList, i);
            SettablePropertyStruct[PropertyName] = 1;
         };
      };
      // Create a structure for storing data values within the object       variables.InstanceData = StructNew();
   </cfscript>
   <cfreturn THIS>
</cffunction>

<cffunction name="asStruct" returntype="struct" access="public" output="false" hint="I return the object as a struct.">
   <cfscript>
      var ReturnStruct = StructNew();
      If (len(GettablePropertyList) GT 1) {
         For (key in GettablePropertyStruct) {
            ReturnStruct[key] = variables.InstanceData[key];
         };
         return ReturnStruct;
      }
      Else {
         return variables.InstanceData;
      };
   </cfscript>
</cffunction>

<cffunction name="dump" returntype="void" hint="I provide cfdump/cfabort functionality within cfscript blocks." output="false" access="private">
   <cfargument name="VariabletoDump" required="true" type="any" hint="The variable to dump.">
   <cfdump var="#VariabletoDump#">
   <cfabort>
</cffunction>

<cffunction name="get" returntype="any" output="false" hint="I provide an interface for getting properties from an object." access="public">
   <cfargument name="PropertyName" type="string" required="true" hint="The name of the property to retrieve.">
   <cfscript>
      var ReturnValue = "";
      If (GettablePropertyList EQ "*" OR StructKeyExists(GettablePropertyStruct, PropertyName)) {
         If (StructKeyExists(variables, "get#PropertyName#")) {
            // Custom getter exists - run it             ReturnValue = evaluate("variables.get#PropertyName#()");
         }
         Else {
            If (StructKeyExists(variables.InstanceData, PropertyName)) {
               // Value exists, return it                ReturnValue = variables.InstanceData[PropertyName];
            }
            Else {
               // Value doesn't exist. Throw error.                throw("BaseObject: #PropertyName# doesn't exist in the InstanceData structure for this object.");
            };
         };
      }
      Else {
         // Not gettable. Throw error.          throw("BaseObject: #PropertyName# is not a gettable property.");
      };
   </cfscript>
   <cfreturn ReturnValue>
</cffunction>

<cffunction name="load" returntype="void" access="public" output="false" hint="I load an object with the values from a struct.">
   <cfargument name="Structure" type="struct" required="true" hint="I am the structure that needs to be loaded.">
   <cfargument name="Prefix" type="string" required="false" default="" hint="Optional prefix on all fields being passed in - if this is passed, ONLY set properties with this prefix.">
   <cfscript>
      var PrefixLength = Len(Prefix);
      var KeyWithoutPrefix = "";
      For (key in arguments.Structure) {
         If (Len(Prefix)) {
            If (Left(Key, PrefixLength) EQ Prefix) {
               KeyWithoutPrefix = Right(Key, (Len(Key) - PrefixLength));
               variables.InstanceData[KeyWithoutPrefix] = arguments.Structure[key];
            };
         }
         Else {
            variables.InstanceData[key] = arguments.Structure[key];
         };
      };
   </cfscript>
</cffunction>

<cffunction name="set" returntype="boolean" output="false" hint="I provide an interface for setting the properties of an object." access="public">
   <cfargument name="PropertyName" type="string" required="true" hint="The name of the property to retrieve.">
   <cfargument name="PropertyValue" type="any" required="true" hint="The value of the property to retrieve.">
   <cfscript>
      var ReturnValue = false;
      If (SettablePropertyList EQ "*" OR StructKeyExists(SettablePropertyStruct, PropertyName))
      {
         If (StructKeyExists(variables, "set#PropertyName#"))
         {
            // Custom setter exists - run it             ReturnValue = evaluate("variables.set#PropertyName#(PropertyValue)");
         }
         Else
         {
            variables.InstanceData[PropertyName] = PropertyValue;
            ReturnValue = true;
         };
      }
      Else
      {
         // Not settable. Throw error.          throw("BaseObject: #PropertyName# is not a settable property.");
      };
   </cfscript>
   <cfreturn ReturnValue>
</cffunction>

<cffunction name="onMissingMethod" access="public">
   <cfargument name="missingMethodName" type="string" required="true">
   <cfargument name="missingMethodArguments" type="struct" required="true">
   <cfscript>
      var MethodName = "";
      If (Left(missingMethodName, 3) EQ "get") {
      MethodName = Right(missingMethodName, (len(missingMethodName) - 3));
      return get(MethodName);
      };
      
      If (Left(missingMethodName, 3) EQ "set") {
      MethodName = Right(missingMethodName, (len(missingMethodName) - 3));
      return set(MethodName, missingMethodArguments[1]);
      };
   </cfscript>
</cffunction>

</cfcomponent>

Comments
Hi Peter,

I notice that you store your instance variables in the variables scope.

Are you storing the object in the applcation scope? If so, how are you maintaining data integrity?
If not, then are would be creating a new object for every request? And consequently, how does the application handle under load?

-j
# Posted By jim | 4/14/08 1:27 PM
There are two types of objects that I create. I have Singletons which live for the life of the application but are injected in using LightWire (a dependency injection framework - like ColdSpring). I also have transients that live typically just for a page request and they get created as and when they are needed.

Basically LightWire handles all of the caching of objects so performance isn't an issue and I don't mix page or user specific state into singletons, so there isn't a problem with data integrity.
# Posted By Peter Bell | 4/14/08 1:58 PM
One thing I've been thinking a bit about lately is pagination within my base IBO object. I've seen a few other implementations of IBO's with pagination built in but I'm not completely sure it belongs there. It certainly could work and may be an easy approach to pagination but I was wondering what your views on the subject are? Have you ever considered putting pagination in your base IBO or do you have another solution?
# Posted By Dustin Martin | 4/14/08 10:09 PM
I have thought about it and I haven't implemented it. Main reason has been I don't cache IBO's, so pagination would be a bit meaningless. Either I provide pagination on the server side, so you're going to create a new IBO with a different set of records for each page request, or I do it on the client side in which case the IBO just displays everything and somethig like a jQuery table with client side paging handles the pagination.

So right now I probably wouldn't add pagination myself, but if you do give it a try, please let me know how it goes!
# Posted By Peter Bell | 4/15/08 11:51 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.005.