By Peter Bell

IBO 2.0 - Return of the Iterating Business Object. Now on RIAForge!

The Iterating Business Object is the basis of almost everything I do in ColdFusion. The ability to load a recordset (or one or more structs or lists) into a single object and then to get all of the benefits of rich encapsulated getters and setters without any of the downsides just gets better and better the more you use it.

My IBO had gotten a little overstuffed with tangential concerns, but as part of a rewrite of my framework I've cut it down to its core concerns while making some improvements to its functionality (adding class properties) and I have decided to release it as a project on RIAForge - ibo.riaforge.org . . .

At it's core, the IBO is a way of encapsulating the getting and setting of properties for a collection of one or more object instances. It means you can just write a User.getAge() method and as long as User extends BaseIBO, you can use the User object for accessing a single User or (using the iteration functionality) a collection of users loaded one struct at a time or (more often) from a recordset.

But what about Scorpio?
I kind of thought the IBO would change pretty radically under Scorpio with the onMissingMethod() feature. After playing with it for a while, I actually still prefer generic getters and setters. Why? Well because almost everything I do is done dynamically, most of my code is looping through property name lists to display or process given properties. Have you ever tried to call get%PropertyName%() in ColdFusion where %PropertyName% is a variable? It is a pain as there is no built in support for variables["get" & PropertyName]() where the variable clause immediately precedes the method brackets. And honestly, even if that was valid syntax, I find "get(PropertyName)" to be more concise and readable than "variables["get" & PropertyName]()".

Of course if you still write a lot of code by hand it may be much more a matter of preference at to whether you prefer get("Whatever") or getWhatever(), but it is worth thinking about. Even without the generic getters and setters, the iterator, information hiding and distinction between class and instance methods can be pretty useful.

Key Features
The core features of the IBO are:

  • Distinct data store - By storing all properties in a struct within the variables scope (variables.data), you never need to worry about name conflicts between properties and methods again.
  • Generic getters and setters - Never write a "default" getter or setter again. Just use get(PropertyName) and set(PropertyName, PropertyValue). If the get#PropertyName#() or set#PropertyName#() methods exist, they'll be called, otherwise the property value will be returned from a single data store.
  • Information Hiding - With the gettableProperties and settableProperties parameters you can set, you can provide a comma delimited list of which properties you will be able to get or set - allowing for information hiding. (The default of * means that you don't have to use this feature if you don't want to).
  • Class Properties - If you have a collection of users, you probable want the first name and last name properties for each one, but you might also want "class" properties telling you things like the title for the object or the list of properties to display on a given screen. class(ClassPropertyName) and setclass(ClassPropertyName) allow you to get/set such properties at any time meaning you can just pass round a self describing bean to your views rather than having to store so much configuration information in your views or other systems. Information hiding also available for class properties with gettableClassProperties and settableClassProperties. Defaulted to * (all) and "" (none) respectively as you'll usually set these as part of the init method (and they are optional constructor arguments in the base class).

API

  • init - This must be called as if it were a true constructor before calling any other methods on the bean. Optional config properties are GettableProperties, SettableProperties, GettableClassProperties and SettableClassProperties.
  • get(PropertyName: string) - If the property name is not in the GettableProperties list, and the list isn't set to "*" for all, it calls invalidAccess() and (if that doesn't throw or dump) returns an empty string to the caller. If the property name is gettable, if there is a custom getter, it calls it. If not it returns the value directly from Data.Instance[PropertyName] if it exists (if it doesn't exist, it also calls invalidAccess()).
  • set(PropertyName: string, PropertyValue: any) - If the property name is not in the SettableProperties list, and the list isn't set to "*" for all, it calls invalidAccess() and (if that doesn't throw or dump) returns an "false" boolean to the caller. If the property name is settable, if there is a custom setter, it calls it. If not it sets the value directly at Data.Instance[PropertyName].
  • class(PropertyName: string) - If the property name is not in the GettableClassProperties list, and the list isn't set to "*" for all, it calls invalidAccess() and (if that doesn't throw or dump) returns an empty string to the caller. If the property name is gettable, if there is a custom getter, it calls it. If not it returns the value directly from Data.Class[PropertyName] if it exists (if it doesn't exist, it also calls invalidAccess()).
  • setClass(PropertyName: string, PropertyValue: any) - If the property name is not in the SettableClassProperties list, and the list isn't set to "*" for all, it calls invalidAccess() and (if that doesn't throw or dump) returns an "false" boolean to the caller. If the property name is settable, if there is a custom setter, it calls it. If not it sets the value directly at Data.Class[PropertyName].
  • invalidAccess(MethodName: string, PropertyName: string) - This method is called if you try to get or set an instance or class property that is not gettable/settable or doesn't exist. By default it throws an error, but you might want to overload or (what the heck) replace it with your own implementation - including the possibility of just failing silently which I used to do but now dislike. I don't usually recommend changing the core files in a project, but for this project - knock yourself out. It's not like the merge for version 3.0 is going to be that hard to figure out :->
  • loadQuery(Query:query) - This adds a query into the IBO after any existing records and then resets the iterator counter to 1 (pointing at the first record). So if you already have ten records, load a query with twenty and you'll have thirty records.
  • loadStruct(Struct:query) - This loads a structure to add a new object instance to the IBO.
  • blendStruct(Struct:query, override:boolean{default:"false"}) - This blends a structure into the current instance being pointed to. You might use this to loadStruct(form) and then blendStruct(url) to merge the URL and Form scopes into a single IBO.
  • loadList(List: string, ) - This is for the special case where you have a comma delimited list of values and want to load then into n-instances. For instance, StateTitleList="Alabama,California . . ." and StateValueList="AL,CA,...". You would loadList(StateTitleList, "Title") and then loadList(StateValueList, "Value") to load an IBO with the state titles and their values.
  • first() - Sets the instance iterator counter to 1 so you can immediately get or set properties of the first instance.
  • reset() - Sets the instance iterator counter to 0 so you can NOT immediately get or set properties. Why use? Because a common way of looping over an IBO is cfloop condition="#Object.next()#" and to use that structure you must Object.reset() first otherwise you'll never have access to the first record.
  • next() - The core of the iterator. Increments the state by 1 and returns a "false" if this is the last record and a "true" if it isn't. SImple but effective iterator.

Samples

Outputting from an IBO to a table:

<cfset User.reset()>
<cfloop condition="#User.next()#">
   <tr>
   <td>#User.get("FirstName")#</td>
   <td>#User.get("LastName")#</td>
   </tr>
</cfloop>

Alternatives
I've been pushing this for a while, but I'm sure I am not the only person who came up with this idea. I know Paul Marcotte created his own implementation and if there are any other approaches to this, please just add a link to the comments below so people can choose the implementation that works best for them.

Download the bean, get your business objects to extend it, have a play, see what you think. Feedback always appreciated!

Thoughts?

Comments
I'm still getting my feet wet in all this OO, getter/setter stuff but this looks pretty neat! I was curious why you would want to hide something ("Information Hiding")?
# Posted By Jim Priest | 6/20/07 10:31 AM
One of the traditional complaints about generic getters and setters is that they make ALL of your properties public. Generally that isn't a best practice as the more properties you expose the more likely you are to break other parts of your code when you make internal changes to your bean.

For instance, you might have a Product.Price which you want to share so you can display Product.get("Price"). You may also have a Product.DiscountAmount, Product.ApplyDiscount and Product.DiscountType that are currently used to calculate the price based on a discount. If they are accessible and you rely on them elsewhere in your code, then you won't be able to change your strategy for calculating discounts without breaking code outside of your product. Unless other parts of your application REALLY need those three properties, they should be private rather than being publicly accessible.

You don't HAVE to use information hiding to use the IBO, but the IBO is designed to support best practices if you want to use them.

Make any sense?!
# Posted By Peter Bell | 6/20/07 10:48 AM
"It is a pain ..." to call methods dynamically.

But, I think for the end user of the IBO, it would be nice to have those methods.

Also, you've given me a good function to put in the base class now - something like "callDynamicMethod" (but better named), which would encapsulate those annoyances.
# Posted By Sammy Larbi | 6/20/07 11:27 AM
Hi Sam,

Yeah, that isn't a bad base function to have. I call it "call" and pass in property name and a struct containing the arguments. I use that all over the place and will probably refactor it into my base object.

As for it being nice for the end users of the IBO, is it really? What is getFirstName() fundamentally superior to get("FirstName")? I'll admit to it being a tiny amount more verbose (the two double quotes are required), but I don't think that is much of a justification. What IS the benefit of getX() over get(X)? If we were going all the way and I could use Ruby like syntax to get Product.Price which would cal the accessor, I'd be willing to say that was just enough syntactic sugar to be worth it, but how much of a difference does it really make whether PropertyName is prepended to get/set or included within the brackets?
# Posted By Peter Bell | 6/20/07 11:34 AM
Well, that's why I want to ditch the get/set idiom and just call Product.Price() or product.Price(15) =)

If keeping get/set, then I agree there is not enough keystroke difference to care. It might be nice as far as visual asthetics go, and there could be no confusion about what is going on. For instance, coming from a Java background, I might see product.get("price") and ask, "is 'product' a HashMap?" Incidentlally, I think that's probably relatively minor, as one would expect that when learning in a language you could be expected to also learn its idioms.

Out of curiosity, what happens if I call get("some_var_used_by_the_base_IBO")? (Did I read that you keep the variables of the child in a special struct?)
# Posted By Sammy Larbi | 6/20/07 12:23 PM
I meant to say "not enough keystroke difference *or clutter difference* to care"
# Posted By Sammy Larbi | 6/20/07 12:24 PM
Peter,

When I took up the crusade to liberate your IBO from "tangential concerns", I hoped you would give it a proper emancipation! Using a self-describing IBO will be a real time saver. Great API!!
# Posted By Paul Marcotte | 6/20/07 7:39 PM
Hi Paul,

Many thanks, but anyone else reading this should check out Pauls implementations as well. From what I can see there are two options:

http://iterator.riaforge.org/
http://recordset.riaforge.org/

It is always good to look at different implementations/approaches!
# Posted By Peter Bell | 6/21/07 5:11 AM
The RIAForge project doesn't allow you to download the IBO CFC. Is that intentional?
# Posted By Clint Miller | 6/21/07 1:10 PM
Hi Cliff,

Apologies. I still haven't got the code up there :-< Locked today, flying to New York tomorrow, the code will be up and waiting for you at the weekend.

Sorry about that!
# Posted By Peter Bell | 6/21/07 1:56 PM
Still no code on RIAForge....
# Posted By sporter | 7/2/07 5:03 PM
Still yet, no code... ha,ha.
# Posted By Aaron Roberson | 7/5/07 4:01 PM
Hi Peter, it's possible to loop over a query if i need navigation (for example looping from record nr 10 to record nr 20)?
Or i need to implement 2 new method in the IBO for modify NumberofRecords and IteratorRecord variables?
Thank you
# Posted By Andrea | 11/15/07 11:26 AM
I considered building this in, but didn't. Please share with me the methods you add if you do this!
# Posted By Peter Bell | 11/15/07 6:10 PM
how about something like this?
I don't implement a pages navigation, but only perform modifies that let me to looping from an x record to an y record.

1)add in loadQuery() method a new var:

variables.LastRecordToView = theQuery.recordcount;

this var set the last record of the query that i want to view in my loop, by default point to the recordcount of the query loaded in the IBO

2)in the next() method:

a)add this condition:

If( LastRecordToView GTE NumberofRecords)
LastRecordToView = NumberofRecords; (so i'm sure that LastRecordToView don't point to a record that doesn't exists)

b)modify the alredy existing condition

"If (IteratorRecord GTE NumberofRecords)"

in

"If (IteratorRecord GTE LastRecordToView)"

so the next() method return false when iterator is equal to the last record i want to view

3)add 2 new method setIteratorRecord(), that modify the variables.IteratorRecord value, and setLastRecordToView(), that modify the variables.LastRecordToView value,
so i can loop from x record to y record:

<cffunction name="setIteratorRecord" returntype="void" access="public" output="false">
   <cfargument name="recordNumber" type="numeric" required="yes">
   
   <cfset variables.IteratorRecord = arguments.recordNumber>
</cffunction>

<cffunction name="setLastRecordToView" returntype="void" access="public" output="false">
   <cfargument name="recordNumber" type="numeric" required="yes">
   
   <cfset variables.LastRecordToView = arguments.recordNumber>
</cffunction>
# Posted By Andrea | 11/19/07 6:05 AM
Hi Peter, sorry, i've another question...
I'm usually start my cfc with the list of the vars and their default values, like:

<cfscript>
variables.userId = 0;
variables.firstName = "";
</cfscript>

In the constructor of my object that extends IBO i have:

<cfscript>
Super.init(); // Super keyword refer to the IBO
...
</cfscript>

but doing this I lost my default value, all the vars return an empty value,
for example

get("userId ");

return an empty value an not the "0" as default in my object.
I'm doing something wrong or it's correct?
thank you
# Posted By Andrea | 11/19/07 10:45 AM
@Andrea,

The FirstRecordToView and LastRecordToView properties in the variables scope were how I was going to implement paging within the IBO. Just default them to 1 and numberofrecords but allow user to set them (probably not just on initialization, but at any time) and it should work well with only a small modification to the reset(), first() and next() methods.

Regarding your properties, have a look at how the IBO stores data. Because there could be n-records, setting variables.UserID isn't going to do anything. Instead you want to first() and then set("UserID", "0") which will use the generic setter to set the first records userID to 0.
# Posted By Peter Bell | 11/19/07 2:12 PM
Thank you Peter,
meanwhile i'm going to use my solution until you write your official implementation for paging to the IBO.
Hi
# Posted By Andrea | 11/20/07 3:43 AM
Hi Peter,
I have this problem setting a Struct.
I post an example code so you can test it:

Component:
==========
<cfcomponent name="Test" extends="BaseIBO">

   <cffunction name="init" access="public" output="false" returntype="Test">
      <cfscript>
         Super.init();
         variables.test=StructNew();
         set("myStruct",test);
      </cfscript>

      <cfreturn this />
   </cffunction>

</cfcomponent>

Page cfm:
========
<cfset t = CreateObject("component","Test")>
<cfscript>
   t.init();
</cfscript>
<cfdump var="#t.get('myStruct')#">

<cfset myQ = QueryNew("id")>
<cfset myA = ArrayNew(1)>
<cfloop from="1" to="10" index="i">
   <cfset myA[i] = i*2>
   <cfset QueryAddRow(myQ,1)>
   <cfset QuerySetCell(myQ,"id",i,i)>
</cfloop>
<cfset QueryAddColumn(myQ,"Valore",myA)>

<cfscript>
   t.loadQuery(myQ);
</cfscript>
<cfdump var="#t.get('myStruct')#">


In the cfm page I have 2 cfdump of the same var (myStruct):
firts one return me an empty STRUCT,
second one, after loading query, return me an empty STRING.
What's wrong?
# Posted By Andrea | 11/29/07 5:46 AM
I just stumbled across this which is great because I was thinking about implementing something similar. It even has the "variable-name prefix" functionality on loadStruct().

@Andrea,

if you're still checking this, loadQuery() and loadStruct() *replace* any existing data in the object, so your second call to get("myStruct") is asking for a property that doesn't exist in the object any more. It returns an empty string because of this line in the constructor:
variables.FailQuietlyonGet = true;

If you want the object to throw an error on bad gets, set this to false. Check line 125 for where it actually gets used.

I, however, was curious about line 20 in the constructor:

Super.Init(argumentsCollection=Arguments);

Which throws errors (Init is not a function) for me unless I comment it out.

Further, I am curious about (also in the constructor):

variables.Data.Instance = StructNew();
...
variables.Data.Instance[0] = StructNew();

Is it intentional to define Instance as a Struct instead of an Array, considering it appears to be used as a standard CF 1-based array everywhere? loadQuery also defines variables.Data.Instance as a struct with integer keys of 1+.

Also, loadQuery() sets THIS.recordset but it is never used for anything? was this intended to be a public variable? If so, reset() and next() should change it as well...
# Posted By Mark Jones | 1/18/08 7:50 PM
Hi Mark,

Great questions! I put this together very quickly to scratch an itch and it has done so well enough that I haven't looked at the code since I hacked this together in a morning. I'm now upgrading my internal framework (of which both this and LightWire are a part), so I'll be going back over the code and re-releasing this over the next couple of weeks.

Apologies for the Super.init(). It is because in my framework IBO extends a BaseObject which does some of the heavy lifting for other objects that aren't IBO's (I actually have generic getters and setters on many of my objects, for example).

I think you're right I should be using an array, but I'll give the code a look over and let you know.

Re: THIS.recordset, it was simply there for debugging (oops!) so I could easily see when I cf dump'd an IBO that it had the data I was interested in. The goal wasn't to access the data through that, but just to be able to see what was up in CF Dump.

Expect a more polished version some time in February (and before my preso's at CF nited Europe in March, Web Maniacs in DC in May and Scotch on the Rocks in Edinburgh in June)- all of which will contain an IBO!
# Posted By Peter Bell | 1/19/08 2:02 AM
I realize this post is quite old but I've recently run into a couple questions that I was hoping you might answer in regards to your IBO. First of all, what is the purpose of the class properties? I don't really understand your explanation on why/when you would use them. You said they could be used to store the "title for the object or the list of properties to display on a given screen". I guess I'm not sure how you could use this. In other words, what good would storing the object title be? Or how would you store which properties are going to be available on a particular screen? Could you maybe elaborate on it a little?

Also, I was noticing that you were storing the data in 'data.instance' as a struct of structs. Why a struct of structs and not an array of structs. I thought I had read elsewhere that arrays are faster. So if that is the case it would seem that an array of structs would be a better option.

All in all I love the idea of the IBO and think your implementation is great. It inspired me to create my own IBO version -- more to just make sure I understand fully what is going than anything. These are just a couple questions I couldn't quite figure out answers to. Thanks.
# Posted By Dustin | 4/8/08 7:42 PM
@Dustin,

Firstly, the IBO is a pattern with a reference implementation rather than a project, so I'm glad you built your own - you can customize it to your specific needs and get to really understand the pattern that way.

As for class data, lets say I have a User object, but I pass it to a generic admin list screen. At the top of the page I was to say "User Admin". I'd say #Object.classget("Title")# Admin" and that will display User Admin just like I want. The goal of class data is data that relates to the object itself - not a specific instance (which in the case of users would be data points like "Fred", "Joe" or "Dustin".

I haven't had any performance problems, so have had no reason to change the data storage structure to date. I'd not worry too much about structs vs. arrays from a performance perspective unless you're running an app and are clearly seeing a performance problem that you can identify as being due to this.
# Posted By Peter Bell | 4/9/08 8:14 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.005.