OOPhoto - Creating Idealized Business Objects
The latest OOPhoto application can be experienced here.
The OOPhoto code for this post can be seen here.
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.
Reader Comments
ben - I love this idea... I feel like having to handle services all over the place is far less than ideal. I am going to give this a shot as soon as time affords... thanks for doing all this thinking out loud!
@Bill,
Glad you're enjoying.
Hey Ben,
Great read so far, I really like where you're going with this. I'm still trying to understand/come to terms with all of these concepts so I can make better choices about application design/flow.
I like the idea of the BO being self aware, but I'm still undecided about the BO using the Service. In other languages these methods might be better suited as static methods, unfortunately we don't have access to static methods in ColdFusion so I see the purpose.
I think part of my problem (I know this is a bit off topic from the BO conversation) lies in the Controller. Rather than creating and loading the object in the controller (in this case the Comment), I think I would rather have the Controller gather up the form data and pass it on to a service for handling letting the service act as the API for the application. This lets me use the controller of my choice (home grown or a framework) and still provides a solid API.
So if the Controller is calling the Service to create/load the new object, does it make sense to have the object turn around and call the service again? Or is there some other way we can mimic static methods to make our BO self aware but still maintain a service layer API?
I'm still trying to understand all of this so I could be way off. Let me know your thoughts.
@Andrew,
I understand your hesitation of having the Controller create and populate the Model (business) objects. This is the same objection that I believe Dan Wilson was having a little while back. It lets us fall back into the "handling data" mindset rather than the "processing data" mindset. A small, but potentially significant difference.
When the Controller does the work, we are handling data, same as we always did. But, when we pass the FORM object off to the Service layer, we are having the Model "process" the data outside of our understanding.
I like the idea at the 1000-foot view, but in actuality, this forces us to couple the Model to the View (both dependent on the same exact form input naming requirements) and takes away any flexibility that the decoupling of MVC afforded us.
That said, Dan did suggest creating an object whose sole purpose was to process the form. This way, the View and the Model would be coupled to this "form processing object" and not to each other.
My question then becomes, "What's the point?" If we are gonna create a unique form processor for the form, why not just have that logic in the Controller? Both seem to be unique to the View, so why add the overhead of a separate object.
See my latest post for a futher exploration of this realization:
www.bennadel.com/index.cfm?dax=blog:1333.view
Anyway, I am just learning all of this myself, so sorry if that was more confusing than helpful :)
@Ben,
Does having the Controller pass data (i.e. from the form) couple the Model to the View? I see the Controller as the translator between the two. Is it the Controllers responsibility to know how to handle Business Objects? Or should it just be translating data for the Service (our API) to handle and accept a result?
I do see a problem, if the Service (Model) is returning the BO, at some point the View will have to know how to use it, so some level of coupling will occur. I'm just wondering if its best to leave as little as possible in the Controller, thus making a move to say a Flex front end that much easier?
As always, thanks for your thoughts
@Andrew,
The way I see it, the translation goes both ways. On one had, the Controller translates return data into View-friendly format. But, on the other hand, the Controller also translates View-Form-Data into business data:
<cfset Gallery.SetName( FORM.name ) />
<cfset Gallery.SetDescription( FORM.description ) />
... etc. ....
Here's where we get into the problem of data "centric" vs. "processing." To get to a more OOP "mentality", we could create another object whose responsibility it is to process the form:
<cfset objErrors = GalleryProcessor.SaveGallery( FORM ) />
Now, we are "asking" objects to do things for us, which is a very OO mindset...
But when I look at this, I ask What is the benefit for the cost? We are creating a totally new object (overhead) for a single form (not reusable) to perform translation logic that was in the Controller before (horizontal movement). To me, it seems that all we do with this strategy is incur overhead without any benefit other than the belief that our Controller shouldn't be doing anything of substance.
@Andrew,
RE:Adding a Flex interface, we have to remember that FLEX and HTML views communicate in much different ways. They both have their own controller. The Flex interface would talk to some sort of RemoteFacade / API that would handle the Flex-specific translation, I assume (I have not done anything with FLEX to date).
Because they are so different, they need to have different returns (I assume). This would all be handled by the RemoteFacade which do its own translation.
@Ben,
I do agree that translation works both ways. What I'm more concerned with is letting the Controller do to much.
I think I would rather see the Controller take the form data and pass it to some API (i.e. FormProcessor or FormService) instead of:
<cfset Comment.load( Form ) />
<cfset Comment.validate() />
<cfset Comment.save() />
...
I'd rather see:
<cfset result = Service.saveComment(
id=form.form_id,
title=form.comment_title
...
) />
While this does look like more code (and yes, it is more overhead) I think it settles better with me (from an API standpoint). I'm still struggling with the cost/benefit of this (like you mentioned above). I'm not sure if a Flex Controller could call Service.saveXX() the same way CF can.
Thanks for your input (I love the conversation).
@Andrew,
I have to say that your little SaveComment( ID=... ) kind of blew my mind :) Using a service to save form data via named-arguments might just be the compromise that settles my stomach.
The way you are suggesting allows the Controller to pass form data into service in a way that:
1. Does not couple the Model to the View (via individual arguments).
2. Encapsulates the way the data is processed from the Model (since it happens in the Service layer).
3. Creates minimal overhead (yeah, we have to create a method for it, but its mostly the overhead of the method signature - not so bad).
This has given me a lot to think about! Thank you.
@Andrew,
The Controller would still need to do *some* stuff, though. In my OOPhoto application, I have a situation where the FORM passes back a list of IDs for photos (the gallery add/edit page). I still would be uncomfortable passing the ID list to the service, but I would be fine with this:
Service.SaveGallery(
. . . . Name = FORM.name,
. . . . Description = FORM.description,
. . . . Photos = Service.GetPhotosByIDList( FORM.photo_id_list )
. . . . ) />
This way, the processing of the data is *still* encapsulated, but the Model doesn't have to realize that FORM.photo_id_list should be turned into an array of Photo objects.
An easy compromise.
@Ben
I agree with you, your example of the id list is kind of a catch. I think I'm alright with another call to get a list of IDs. I think you'll have to do something similar for things like statuses, etc. that are persisted. But in any case the Controller still doesn't know about the Model, only about the API its been provided (1 or multiple calls).
I'm not sure if this is really a true Service, or if it should hold another name, but regardless I like having an API. I still need to try out Flex to see how well the translation will work.
@Andrew,
I tried to sum up this sub-conversation with its own post. Check it out:
www.bennadel.com/index.cfm?dax=blog:1334.view
Thanks for all your feedback and suggestions.