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
Peter,

Do you have a way of deleting a record from an IBO?

Thanks,
Seth
# Posted By Seth | 12/9/08 11:38 AM
I don't because it's not how I use them. I load them transiently rather than caching them and don't make them do all the singing and dancing that some others want to do (which is also a valid approach - just not what I'm doing).

That said, you absolutely could add methods to an IBO to delete a record, find by property value, find by filter (using QoQ), delete records matching filter criteria, etc. Heck, if you had enough RAM and good enough uptime I guess you could use them to replace the db (or in the real world at least to cache the heck out of it). But it's not a good fit for how I happen to use them.
# Posted By Peter Bell | 12/9/08 11:55 AM
Peter,

I wonder what you think of my way of creating the getable and setable information?

http://www.objectiveaction.com/Kevin/index.cfm/200...
# Posted By Kevin Roche | 1/26/09 4:24 PM
Hi Kevin,

I think this is a great approach and it's the direction I'm moving towards in CF as it seems to fit with where CF9 is going in terms of making CF Property the definitive source for metadata. The main practical issue is that you may need to rename some of your metadata for CF9 if you end up using the same names as Centaur uses for some of these things, but quite frankly, a lot of this work is going to be moot in CF 9 as I'm pretty sure there has been public talk about adding ideas like implicit getters and setters, so I think this is mainly going to be a patch for CF8 until CF9 comes out.

I definitely think this is the way to go though.
# Posted By Peter Bell | 1/27/09 8:19 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.005.