Objects and Frameworks - The Service Layer
I have discussed in past posts on this topic (i.e. OO and frameworks) that often things begin to make more sense once you understand the need for them. For instance, ColdSpring makes much more sense once you see the difficulty of managing dependencies within your application. However, if you are new to OO, ColdSpring may seem to add another layer of complexity without much payoff. Well, this is how I came around to understanding the service layer, I saw the problem first.
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):
<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:
<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 :)
Nice post!
To enforce "service as API" it is often worth making all DAO and Gateway methods "package" (protected for the Java heads) and putting UserService.cfc, UserGateway.cfc, UserDAO.cfc and UserObject.cfc in the same directory.
Oh, but could you simplify the captcha?! Pete provides an XML file so it is really easy to do and Charlie has provided step by step instructions . . .
http://carehart.org/blog/client/index.cfm/2006/8/17/simplifying_lyla_in_blogcfc
Looking forwards to the ColdSpring post!!!
Best Wishes,
Peter
http://www.robgonda.com/blog/files/robGonda/UserFiles/Image/layers%20architecture.jpg
Cheers.
I think this was a great if it was written with the sole purpose of arguing for the benefits of using a service layer.
I think I understand service layers, and was benefited by the post, but I believe you originally set out to introduce these concepts to beginners in this series. If I didn't already know what a service layer was and how to use it this article would have made me want to learn how to code up a service layer due to the benefits so nicely articulated, but I would still be at a loss for how to write one for my own apps.
-Aaron
Nonetheless, if you want to see the basic skeleton of a service layer component, check out my code generator which will auto-generate some very basic service methods for you - http://code.google.com/p/cfcgenerator/
Thanks for the post. I learned the hard way myself - a very bloated controller in MG.
I'd love to see an intermediate or advanced version of this article. Do you think you might do this in the future?
LT


