Photo from Chile

How I Use Transfer - Part III - Abstract Objects

In the previous post in the series I discussed the four types of objects that comprise my model. As I was creating instances of those object types I realized that they did a lot of the same things. For example, my UserService needed methods like getUser(), updateUser(), deleteUser() and listUsers(), while my ProductService needed getProduct(), updateProduct(), deleteProduct() and listProducts(). Not only do they need to do similar things, but they pretty much do them the same way. Now that's a lot of code duplication.

So I created an AbstractService, which each of my concrete Service Objects extend. My AbstractService has methods like Get(), Update(), Delete() and GetList(). For the most part, the code required for getUser() is identical to the code required for getProduct(), with the only difference being the business objects with which they interact (User and Product, respectively), so I was able to write a single, parameterized method, which will work for most of my Service Objects.

I call this object abstract because it shouldn't be instantiated on its own. It is used solely as a base object which is extended by most of my concrete Service Objects. The methods in this objects are designed to work inside of the objects which extend it, but it is also totally acceptable to extend (via super) or override these methods in the objects which extend it. So, neither my UserService nor my ProductService contain a Get() method, but my ReviewService does. It contains a Get() method that overrides Get() in the AbstractService.

I'm not going to describe my entire AbstractService in this post, but let's look at the Get() method as an example of what I'm talking about:

view plain print about
1<cffunction name="Get" access="public" returntype="any" output="false" hint="I return a Business Object, ready to be used.">
2    <cfargument name="theId" type="any" required="yes" hint="The Id of the business object" />
3    <cfargument name="needsClone" type="any" required="false" default="false" hint="Should a clone be returned?" />
4    <cfargument name="args" type="any" required="no" default="" hint="A package of data to load into the object" />
5    <cfargument name="TransferClassName" type="any" required="no" default="#getTransferClassName()#" hint="The class name of the business object, as defined to Transfer" />
6    <cfargument name="FieldList" type="any" required="no" default="" hint="A list of fields to populate" />
7    <cfset var theTO = 0 />
8    <cfif arguments.theId EQ 0>
9        <cfset theTO = getTransfer().new(arguments.TransferClassName) />
10    <cfelse>
11        <cfset theTO = getTransfer().get(arguments.TransferClassName,arguments.theId) />
12        <cfif arguments.needsClone>
13            <cfset theTO = theTO.clone() />
14        </cfif>
15    </cfif>
16    <cfif IsStruct(arguments.args)>
17        <cfset theTO.populate(arguments.args,arguments.FieldList) />
18    </cfif>
19    <cfreturn theTO />

So, what's going on here? This method actually serves three use cases:

  1. I simply want to retrieve a Business Object for display purposes
  2. I want to retrieve a Business Object because I want to create a new instance of the object
  3. I want to retrieve a Business Object because I want to update an existing instance of the object

Let's skip past the arguments and look at the code. First if I want to create a new object, then the Id passed in will be 0 (for the most part), so in that case I simply call getTransfer().new(), passing in the TransferClassName. Otherwise I want to return an existing object from Transfer so I call getTransfer().get(). If I'm working with an existing object, I may want to manipulate that object, in which case I want to create a clone() of it and return that. So now I've got my Transfer Object and I need to decide whether to return it as is, or to fill it with data. If I have passed in an argument called args, which is a struct, then I call the populate() method on my Transfer Object to load that data into the Transfer Object before returning it. That populate() method does a bunch of different things, which I'll cover in a future post. Don't worry about arguments.FieldList for now, most of the time it remains an empty string and isn't used.

So, there's an example of a method in an abstract object. It achieves three different things, and it will work with almost all of my concrete Service Objects. In fact, in my current application I only need to override this method in one of my concrete Service Objects. The rest inherit it directly from the AbstractService object.

The other nice thing about this Get() method is that it allows me to encapsulate my communication with Transfer. This Get() method is kind of like a Business Object factory, in that the logic required to create Business Objects is limited to this one method. If I were ever to stop using Transfer (gasp!) and choose to move to a different ORM, little about my service layer would have to change.

I know I've left a couple of things out in my explanation of the Get() method, but I think this simple introduction to abstract objects has already become less than simple. I'll go into more detail about the implementation of this object in the next post.

Getting back to the four object types mentioned in the previous post, I also have an AbstractGateway which all of my concrete Gateway Objects extend, as well as an AbstractTransferDecorator object, which is used as a base object for all of my concrete Transfer decorators. Because I rely on Transfer to create most of my business objects my AbstractTransferDecorator is really like an abstract Business Object. There are no abstract objects for my Utility Objects as they are all standalone objects that each serve a single purpose.

To recap, my model consists of 4 object types:

  • Service Objects, most of which extend AbstractService.cfc
  • Gateway Objects, all of which extend AbstractGateway.cfc
  • Business Objects, most of which are based on AbstractTransferDecorator.cfc
  • Utility Objects, which stand alone

In the next installment I'll take a closer look at the AbstractService Object.

Good stuff. Real good stuff.
# Posted By John Allen | 6/30/08 12:42 PM
I really appreciate you going through this. It is really helping to crystallize my learning how to use Transfer, and its place in an "OO" CF app.

Any chance validation (the different places and types) and collections (when to use objects, queries, or an iterator) are on the table to present?
# Posted By Daniel Kim | 6/30/08 2:39 PM
Thanks for the kind words, John and Daniel. I'm glad to hear that it's of value.

I will be discussing how I'm currently dealing with validation. I'm not really that happy with my current implementation, but haven't taken the time to improve it yet. Perhaps this will give me the kick in the pants I need. A before article and an after article, perhaps.

I wasn't planning on discussing collections much, as those choices are pretty specific to your project's requirements. At this point I'm just trying to cover the basic groundwork - stuff that is common to all (or most) of my projects.
# Posted By Bob Silverberg | 6/30/08 5:38 PM
Excellent stuffs!

Wanted to print this out for reading material on the train but the code is not showing up?
# Posted By felix tjandrawibawa | 6/30/08 9:29 PM
Thanks Felix. The print stylesheet has now been fixed to display the code.
# Posted By Bob Silverberg | 7/1/08 5:20 AM
Hi Bob

came here via Seans blog. Very useful series for me which clarifies a lot of things. Will be following with interest.

A quick question, a bit unrelated, but you're normally forthcoming with this sort of thing ;) What is the difference between using new() to create a new object and using get() with an id you know doesn't exist. I don't use new() at all, but I could be missing something.
# Posted By richard | 7/5/08 12:40 AM
Hi Richard,

I'm glad that you're finding the series useful. I'm not much of an expert on the internals of Transfer, but I'm fairly sure than new() is less expensive than get(). With get() Transfer has to do at least two additional things:

1. Check the cache to see if the object is already there
2. Query the database for the object

That second one in particular is what I'm trying to avoid. If you have debugging turned on to see all of the SQL that Transfer is sending to the database, you'll see a SELECT xxx WHERE id=0 for all calls to get("myclass",0). Transfer cannot cache the fact that record 0 does not exist, so it must issue that query each time.

Please let me know if that doesn't answer your question.

# Posted By Bob Silverberg | 7/5/08 4:52 AM
Bob - that answers my question perfectly. I hadn't considered the performance overhead, so in future I will factor that into my particular use cases.


# Posted By richard | 7/5/08 10:16 AM
Bob - just had another quick (obvious) thought about this. The benefits of always using get() mean leaner code - no checking if an object exists. However if this is abstracted, as in your example, these benefits fade. And no need to examine use cases to decide if there is a trade-off
# Posted By richard | 7/5/08 11:08 AM
When I first started down this path I did just use get(), for the reason you mention. It seemed very convenient that I could just call get() every time and it would deal with missing objects for me. But then I observed the SQL being generated and wanted to find a way to eliminate all of those unnecessary calls to the database, so I introduced the condition.

# Posted By Bob Silverberg | 7/5/08 12:29 PM
Great series, I've really enjoyed reading it and learned a lot. Thanks!
# Posted By Rachel Lehman | 7/6/08 8:03 PM
Thanks Rachel. I'm glad it is of value to you.
# Posted By Bob Silverberg | 7/7/08 10:09 AM