After discussing what makes an object "ideal," I went ahead and idealized my OOPhoto business objects. Because my application is so small, that really didn't involve a whole lot of work. Would you believe that I made the entire shift in 30 minutes (or less)? Basically, all I had to do was move my Save() and Delete() methods into the business objects and change the way that they were being accessed. Even changing the way they were accessed was a minimal effort; it didn't take much more than an extended-find in HomeSite and turning code like this:
<cfset ARGUMENTS.Data.Cache.Factory .Get( "CommentService" ) .Save( LOCAL.Comment ) />
... into code like this:
<cfset LOCAL.Comment.Save() />
As I discussed before, the first step in making my business objects "ideal" objects is simply creating the proper object API. In reality, the business objects are just turning around and executing the same methods on the appropriate service objects. To see what I am saying, take a look at the Delete() and Save() methods of my Comment.cfc:
<cffunction name="Delete" access="public" returntype="any" output="false" hint="I delete this comment."> <!--- Pass command to service class. ---> <cfreturn VARIABLES.CommentService.Delete( THIS ) /> </cffunction> <cffunction name="Save" access="public" returntype="any" output="false" hint="I save this comment."> <!--- Pass command to service class. ---> <cfreturn VARIABLES.CommentService.Save( THIS ) /> </cffunction>
Hardly anything going on. The Comment bean just calls the appropriate method in the Comment Service object and passes itself (THIS reference) to the service.
While very little is going on here, it does have some implications. For starters, we still have to be able to execute our database calls with transactional functionality. If you recall from my previous post on this topic, I accomplished this by adding a Transaction behavior to any method call that was suffixed with, "WithTransaction". Since these functions do not exist, I had to duplicate some of the OnMissingMethod() functionality from the BaseService.cfc in my BaseModel.cfc. Likewise, I had to duplicate the ExecuteWithTransaction() utility method as well.
At first, I thought that this was a slippery slope; now I'm starting to duplicate core functionality across objects - is this going to get out of hand?!? Am I gonna start duplicating features all over the place? Then, I took a step back into reality and realized that this fear was totally unfounded. The are only a limited number of object "types" in my application. And of those, only two of them will have any database related functionality - the two that I have been discussing: Service objects and Business objects. As such, this is all the duplication of database functionality that I will ever need to do; and so ends the fear of duplication.
And besides, I'm not really duplicating much functionality at all. The business logic surrounding the data manipulation and persistence is still contained in the Service layer; I am simply providing different touch points to that functionality.
One of the things that I really like about this style of programming is that I don't need to always be getting references to an object's Service class. Invoking methods on the Business object helps to encapsulate the relationship between the service layer and the business layer. This means that should that relationship need to change in some way down the road, our code changes will be kept to a minimum.
And of course, the code is shorter. If you look at the first code sample in this blog post, calling Save() on a business object is significantly shorter than calling Save() on its Service class. I am not sure that the brevity of code should really be a selling point, but let's face it, writing shorter code is much more enjoyable than writer longer code.
Now here's the question - do we even have to put these methods in the business objects? If we have a pointer to a "MyService" service object, we could use the OnMissingMethod() functionality provided by ColdFusion 8 to make this very dynamic. We know that the Save() and Delete() methods always return the result of the service class; as such, we could, theoretically, just move all of this logic into the OnMissingMethod() method and have it turn around and call the appropriate functions on the MyService pointer.
Hmmm, food for thought. I am already creating much of business object's API via OnMissingMethod(), so clearly, I am not worried about an auto-documenting API. Perhaps this would be an interesting next step to take.
Object Oriented Reality Check
Adding business object hooks to the Save() and Delete() methods required a little code duplication and minimal effort. But, did we accomplish anything? Let's take a moment and think about this.
Was This Step Worth Implementing?
I think absolutely, yes. While we didn't really add functionality to the system, per say, what we did was create uniformity in our API architecture. Object Oriented programming is definitely an art form; but, I feel that a little too much of it seems to be subjective. As such, I have put a lot of thought into how to deskill some of the decisions that we need to make. In my previous OOPhoto post, you will see that I have come to the conclusion that any method that uses or leverages the data of an object should be invocable on that object. By using this as a rule across the board, not only do we set up a consistent feel for the API, we now afford ourselves the ability to explain our actions using something other than, "It Depends."
Is My Application Object Oriented?
Putting these method hooks in our business objects was done to make our objects appear more "Ideal". And, since an object in Object Oriented Programming (OOP) is supposed to be an idealized version of its real-world counterpart, I have to believe that this step moves us closer to a true object oriented system. But are we there yet? We have smart objects. We have all of our data loading, persistence, and validation logic in our service layer. We no longer have any business logic in our Controller. If this isn't object oriented programming yet, I have to believe that it's getting very close.
Want to use code from this post? Check out the license.