It's been probably 3 months since I started using the CFWheels framework, and I have nothing but praise for it. It's a compact conventions-based framework patterned after Ruby on Rails that, once you have a grasp of the relatively simple yet complete (mostly) API, makes putting an app together a pleasure. Having come to CFWheels from other frameworks that I've been using for a few years now (Model Glue and Coldbox primarily), I was a bit taken aback when I discovered that not only was there no provision made in CFWheels for utilizing service layer objects, but every other CFWheels developer I communicated with about the apparent lack gave me the virtual two dollar look and wondered why on earth I would ever need a service layer object in my application! Apparently, or so it seems to me thus far, I am the ONLY CFWheels developer on the planet who ever saw a service layer object as a necessity! Now, I don't believe that is true (how could it be?), but since I've encountered no others, I felt the need to do a short series of blog posts covering my view of service layer objects, why they're potentially necessary in ANY app (including a CFWheels app!), and two ways in which I overcame CFWheels' ostrecization of service layer objects.
This post will cover my personal philosophy on service layer objects and use cases for them. I'll cover the remaining information in subsequent posts.
note: my personal belief on the necessity of service layer object only holds water IF a person, like me, holds religiously to the philosophy that controllers as a rule should remain as thin and code-free as possible, and that as much business logic as possible belongs in the app's model, not in its controllers. Additionally, that the controller's job is to act as liason between the view and the model, nothing more.
SO, what is a service layer object? I evolved my personal definition of this creature literally years ago, and in all my subsequent experience with OOP, that definition has not changed one iota. A service layer object is an object which utilizes one or more child or even sibling objects in order to encapsulate and perform some genre of work for your applicaton. My favorite analogy is that of a man in an easy chair with three remotes on the tv table beside him. His wife (the application) commands him to "start the movie" (calls his "startMovie()" method). The man (the service layer object, "objHusband") picks up the TV remote (objTV) and turns it on (objTV.powerOn()), changes the input source (objTV.changeInputSource(3)), then adjusts the volume (objTV.adjustVolume(15)). He then takes the DVD remote (objDVD) and powers it on (objDVD.powerOn()), and starts the movie (objDVD.Play()). This husband provided his wife with a single API method ("startMovie()"), while he himself (objHusband) internally manipulated at least two other autonomous class instances (objTV and objDVD) in order to accomplish that piece of work. THAT is the beauty, purpose, and reason for, service layer objects. Without the husband, the application/wife would have had to talk to two other objects and manipulate them herself...heaven forbid! And, if at any point in time the objTV had been replaced and the buttons weren't the same, the wife wouldn't be able to start her own movie at all! But with the Husband service layer, she doesn't ever need to care about how to work the TV remote...that's the service layer's job, and that is where any and all code related to TV manipulation live and would be modified if the need arose. It's beautiful, isn't it? And if the solution is beautiful, it is right.
Now, where in the real world of apps does this same situation occur? In my world, and I KNOW it must be so in your world too, it occurs ALL THE TIME! As an example, allow me to share the skinny on an app I wrote a while back. This app is a tool that allows multiple users belonging to multiple different client subscriber companies to upload their spreadsheet full of insurance claims into a third party system. This third party system has a rather painful upload lifecycle that requires that each claim be in the form of an XML file that conforms to their DTD. Each xml file/claim is then FTP'd to a test server where, after some unknown time period, their cron job comes along and parses it to test for errors. The cron job leaves behind a text file named the same as the xml file indicating if it passed its test, and if not, what errors were encountered. My app has to poll for these test results, grab them, parse them, and return meaningful information to the user. If the xml file passes, it is then allowed to be FTP'd to the third party's production server where the same process takes place again. That's it in a nutshell. Ugly, eh? But even with such a complicated, ugly process to have to deal with, it can be managed well code-wise if we carefully group like functionality together and encapsulate it in a logical way. Working through the process/code needs to accomodate the process described, you would (as did I) come up with an architecture that is composed of several very purposeful, focused CFCs, and a few higher level CFCs that incorporated and manipulated these more specific CFCs (this is called "aggregation"; my service layer component "aggregates" my single-focus CFCs). My own solution resulted in the following CFCs, all of which reside within my app's "model" folder:
I won't delve into any more detail than this, but suffice it to say that, there is a LOT Of work that is going on that does NOT relate directly to ANY DATABASE TABLE! Now, if a CFWheels developer (any of the ones I have communicated with) were to write this same app using that framework, there's only a few probable things that could occur: They would end up with controllers that are bloated beyond belief, possibly trying to call one another for different functionality, OR they would have created a whole slough of UDFs somewhere and had to include them in their controllers, OR they would have had to write CFWheels plugins to handle the meatier parts of the work. Any of those three approaches, to me, are either: wrong, ugly, far more work than should be necessary, or any combination thereof. CFWheels, like any other app, SHOULD provide for easily implementing service layer objects, not force developers to work around this fact! I should be able, within my controller OR from within my service layer object, be able to call "<cfset objSpreadsheetService = model("spreadsheetService"), and NOT get an error telling me there's no corresponding table! That's my belief, anyway.
So, based on my personal OOP beliefs, and using this real world example as my use case (though I have several others as well), I conclude that there exists a definitive need for CFWheels to accommodate service layer objects and to expand its definition of what an app's model truly is, as encompassing not only the CFCs representing discrete database tables, but also any and all other business logic that is unique to the application's identity.
All that said, I do realize that CFWheels is patterend after ROR, and typically when I ask questions like this, the first response is to ask "how does RoR handle that"? I don't know the answer to that, and perhaps because this IS a RoR-patterned framework I shouldn't even WANT it to accommodate service layers as I know them to be. In any event though, I DID convince CFWheels to allow me to code the way I want to. Coming up next, the second post in this series covering the first approach I used to get CFWheels to respect my need for service layer objects.