Two Utility Classes to Help Keep Your Controllers Lean

2008 January 23
by Paul Marcotte
I have really enjoyed reading the perspectives on application architecture that have sprouted up on the mailing lists lately. It comes a at good time for me, since I'm beginning my first fully OO MVC project. One of the best recommendations, I feel, is to keep your controllers as dumb as possible. While this is a fine aspiration, there are plug-ins and convenience methods available to frameworks that just make life easier. In particular, the availability of "beaner" methods. I'm currently using Model-Glue, so I'll use the makeEventBean() method as an example. MakeEventBean() is really useful for populating a bean with any setter methods that match the keys in the Model-Glue event. While convenient, I think it provides a slippery slope for business logic to slide into controller methods.Here's an example. <cffunction name="doRegister" access="public" output="false" returntype="void">
<cfargument name="event" type="any">
<cfset var local = StructNew() />
<cfset local.user = getUserService().newUser()>
<cfset arguments.event.makeEventBean(local.user)>
<cfset local.errors = local.user.validate("register")>
<cfif StructCount(local.errors) eq 0>
<cfset getUserService().save(local.user)>
<cfset arguments.event.addResult("success")>
<cfelse>
<cfset arguments.event.setValue("errors",local.errors)>
<cfset arguments.event.addResult("failure")>
</cfif>
</cffunction>
As it stands, that is the smallest I could make my controller method while taking advantage of makeEventBean(). I was able to do this, because I kept all validation in my business object. To make my controller even dumber, I put together a couple of classes that provide beaner functionality that can be injected into services and better messaging between controllers and services. For lack of better names, I'm calling them Beaner and ServiceResult. These are pretty small classes, so I'll include the full code to each and an example use case. Beaner.cfc <cfcomponent display="Beaner" hint="I set the properties of an object matched in a collection.">

<cffunction name="init" access="public" output="false" returntype="Beaner">
<cfargument name="excludeKeys" type="string" required="false" default="">
<cfset setExcludeKeys(arguments.excludeKeys)>
<cfreturn this />
</cffunction>

<cffunction name="populate" access="public" output="false" returntype="any">
<cfargument name="bean" type="any" required="true"/>
<cfargument name="propertyCollection" type="struct" required="true"/>
<cfset var k = "">
<cfloop collection="#filterKeys(arguments.propertyCollection)#" item="k">
<cfif structKeyExists(arguments.bean, "set#k#") >
<cfinvoke component="#arguments.bean#" method="set#k#">
<cfinvokeargument name="#k#" value="#arguments.propertyCollection[k]#" />
</cfinvoke>
</cfif>
</cfloop>
<cfreturn arguments.bean>
</cffunction>

<cffunction name="checkInputDataType" access="public" returntype="struct" output="false">
<cfargument name="bean" type="any" required="true"/>
<cfargument name="propertyCollection" type="struct" required="true"/>
<cfset var argTypes = getMethodArgumentTypes(arguments.bean)>
<cfset var i = "">
<cfset var errors = StructNew()>
<cfset var message = "">
<cfloop collection="#filterKeys(arguments.propertyCollection)#" item="i">
<cfif not IsValid(argTypes["set"&i],propertyCollection[i])>
<cfset StructInsert(errors,i,"The value '"& propertyCollection[i] & "' for " & i & " is not of type " & argTypes["set"&i])>
</cfif>
</cfloop>
<cfreturn errors>
</cffunction>

<cffunction name="getMethodArgumentTypes" access="private" output="false" returntype="struct">
<cfargument name="bean" type="struct" required="true">
<cfset var functionNames = StructKeyArray(arguments.bean)>
<cfset var i = 0>
<cfset var functionMetadata = StructNew()>
<cfset var thisMetadata = "">
<cfloop from="1" to="#ArrayLen(functionNames)#" index="i">
<cfif Left(functionNames[i],3) eq "set">
<cfset thisMetadata = GetMetadata(bean[functionNames[i]])>
<cfset structInsert(functionMetadata,functionNames[i],thisMetadata.parameters[1].type)>
</cfif>
</cfloop>
<cfreturn functionMetadata>
</cffunction>

<cffunction name="filterKeys" access="private" output="false" returntype="struct">
<cfargument name="propertyCollection" type="struct" required="true"/>
<cfset var i = "">
<cfloop collection="#propertyCollection#" item="i">
<cfif ListContains(getExcludeKeys(),i)>
<cfset StructDelete(arguments.propertyCollection,i)>
</cfif>
</cfloop>
<cfreturn arguments.propertyCollection>
</cffunction>

<cffunction name="getExcludeKeys" access="private" output="false" returntype="string">
<cfreturn variables.excludeKeys />
</cffunction>

<cffunction name="setExcludeKeys" access="private" output="false" returntype="void">
<cfargument name="excludeKeys" type="string" required="false" default="">
<cfset variables.excludeKeys = arguments.excludeKeys>
</cffunction>

</cfcomponent>
Beaner provides two public methods, populate() and checkInputDataTypes(). Both methods require a bean and a propertyCollection (structure) as arguments. If you don't trust the input and want to validate it against the data type for the aguments of setter methods it is destined to populate, you can first run checkInputDataTypes(), which will return an error collection. Otherwise, you can simply populate() your bean. I also added a constructor argument that allows one to set a list of keys to globally ignore when populating a bean or checking datatypes. So, for my Model-Glue example, I have a couple of ColdSpring bean definitions that look like this: <bean id="beaner" class="model.util.Beaner">
<constructor-arg name="excludeKeys"><value>EVENT,myself,eventValue,self</value></constructor-arg>
</bean>
<bean id="UserService" class="model.user.UserService" lazy-init="false">
<constructor-arg name="beaner">
<ref bean="beaner"/>
</constructor-arg>
</bean>
The second class is something that I've heard Brian Kotek mention on a few occasions. I didn't get the usage for it at first, but it made sense when I started to think of the model servicing multiple clients (controllers, Ajax proxies, Flex front ends). This is really the heart of the argument to keep the controllers lean and keep the business logic in your services (and business objects they manage). ServiceResult.cfc <cfcomponent displayname="ServiceResult" output="false" hint="I am a simple result object returned by service method invokation.">

<cffunction name="init" access="public" output="false" returntype="ServiceResult">
<cfset variables.result = "">
<cfset variables.errors = StructNew()>
<cfreturn this />
</cffunction>

<cffunction name="setErrors" access="public" returntype="void" output="false">
<cfargument name="errors" type="struct" required="true">
<cfset variables.errors = arguments.errors >
</cffunction>

<cffunction name="getErrors" access="public" returntype="struct" output="false">
<cfreturn variables.errors />
</cffunction>

<cffunction name="setResult" access="public" returntype="void" output="false">
<cfargument name="result" type="any" required="true">
<cfset variables.result = arguments.result >
</cffunction>

<cffunction name="getResult" access="public" returntype="any" output="false">
<cfreturn variables.result />
</cffunction>

<cffunction name="getSuccess" access="public" output="false" returntype="boolean">
<cfset var success = true>
<cfif StructCount(variables.errors) gt 0>
<cfset success = false>
</cfif>
<cfreturn success>
</cffunction>

</cfcomponent>
The premise for this class is that when I invoke a service method from some client code, I expect it to return a ServiceResult which I can then use for decision points. I don't know that I would use this for all service communication, but for methods that are heavy with business logic, I will definitely use them. Here's what my controller method looks like after implementing the Beaner and ServiceResult. <cffunction name="doRegister" access="public" output="true" returntype="void">
<cfargument name="event" type="any">
<cfset var result = getUserService().registerUser(argumentCollection=arguments.event.getAllValues()) />
<cfif result.getSuccess()>
<cfset arguments.event.addResult("success")>
<cfelse>
<cfset arguments.event.setValue("errors",result.getErrors())>
<cfset arguments.event.addResult("failure")>
</cfif>
</cffunction>
And now that the heavy lifting is in the service, here's the service method (including data type checking that I hadn't used previsously). <cffunction name="registerUser" access="public" output="false" returntype="any">
<cfset var user = newUser() />
<cfset var result = getTransientFactory().createTransient("serviceResult")>
<!--- first test data types --->
<cfset result.setErrors(getBeaner().checkInputDataType(user,arguments))>
<cfif (result.getSuccess())>
<!--- populate the bean --->
<cfset user = getBeaner().populate(user,arguments)>
<!--- validate for register --->
<cfset result.setErrors(user.validate("register"))>
<cfif (result.getSuccess())>
<!--- save the user --->
<cfset save(user)>
<!--- start session --->
<cfset setUserSession(user.getId())>
</cfif>
</cfif>
<cfreturn result>
</cffunction>
What I like about having a convenient beaner in the service and a generic result object is that if I needed to change my registration form to an ajax form, my service proxy method would be very similar to the controller method with the exception that it would likely return as JSON with keys, 'success' and 'errors' rather than set values on an event object. I'm not really into new year resolutions, but I am going to put my controllers on a diet and promote a healthier application architecture, by moving logic out of the controller and into the model. :)