Objects and Frameworks - The Service Layer
I was building my first significant Model-Glue application following an example that someone had posted online. While it was a great example for beginners of building a small application, it had all of the application's business logic in the controller/listener. As my application progressed, I started thinking how this "über-controller" was making my application more maintainable. Yes, I could make multiple controllers, but even then they would each be huge and didn't seem very easy to maintain. Also, what if I moved from Model-Glue to another framework? The controller CFC has Model-Glue specific code and would mean that everything would have to be heavily modified or rewritten. Plus, other than copy/paste, I couldn't really reuse much of this code. This just didn't seem like the right way to do things.
Well, after some direction from Peter Farrell, I realized it was not. First of all, my controller/listener CFC should be as dumb as possible - meaning it should ideally contain no business logic whatsoever. It should say, you want to get users, I have a users service and I will call the getUsers() method and return the results. Here is an example function from my Mach-ii listener (i.e. controller) from my CFC generator application that gets a list of available datasources (on a side note, my generator actually does generate a *very* generic service component for you to modify):
<cffunction name="getDSNs" access="public" returntype="struct" output="false"> <cfargument name="event" type="MachII.framework.Event" required="yes" /> <cfreturn variables.adminAPIService.getdatasources() /> </cffunction>
All this does is call the appropriate function from the appropriate service. It is interesting to note, this would look essentially the same for Model-Glue. The service layer would now contain any business logic required to complete this task, calling any beans, DAOs and Gateways as needed.
If we are talking about users, for example, you can think of the service as the users API. So, for users, you can even begin to think of what you would need from an users API...I need to get a user, get a list of users, save a user, delete a user and perhaps validate a user. So, what would your service look like? Let's borrow an example from Kurt Wiersma's excellent AppBooster mach-ii starter application, it has getUser(), getUsers(), saveUser() and authenticateAdminUser() like we discussed might be needed for the API. Now let's look at the method that handles getting a user:
<cffunction name="getUser" access="public" returntype="appbooster.model.user.user" output="false"> <cfargument name="user_id" type="numeric" required="yes"> <cfset var user = ""> <cfset var roles = 0> <cfset var i = 0> <cfif arguments.user_id neq 0> <cfset user = variables.userDAO.read(arguments.user_id)> <cfset roles = user.getRoles()> <cfif user.getAddress().getID() neq 0> <cfset user.setAddress(getAddress(user.getAddress().getID()))> <cfloop from="1" to="#arrayLen(roles)#" index="i"> <cfset roles[i] = variables.roleService.getRole(roles[i].getID())> </cfloop> <cfset user.setRoles(roles)> </cfif> <cfelse> <cfset user = createObject("component", "appbooster.model.user.user").init()> </cfif> <cfreturn user> </cffunction>
So, in this case, we are getting a user and his associated security roles. You can see all the logic for handling this lies inside the service component, making it easy to maintain while improving reusability and portability. Why? Well, for example, because a user service is something I may commonly use throughout different applications, and therefore I may reuse/repurpose this userService.cfc in multiple applications. Also, this service object would look exactly the same regardless of what framework it existed on.
So, hopefully this has served as a good general introduction to the service layer and now you are imagining your controller/listener just calling a bunch of service components that are loaded in the cunstructor/init (or setters). These service components call DAOs and Gateways, which I also set in the cunstructor/init (or setters). You may be asking, doesn't it start to become a lot of trouble maintaining all these dependencies? Aha! Hello ColdSpring!..but that is a discussion for another post :)
