Recently I've been hard at work trying to understand the core principles of OO programming in ColdFusion. Last week Ben Nadel shared his experiences at a week-long course devoted to OOP with Hal Helms (the summary, which can be found here, also has links to his daily briefs). His posts opened my eyes to a couple of concepts that are now new to me, and since I'm on a new project I thought it'd be a great opportunity to try them out. As you know from my previous posts, I'm also trying to implement IoC (Coldspring) and ORM (Transfer-ORM) frameworks into my project, so figuring out where these two stand in relation to what I think I now know about OOP is my next challenge.

Singleton vs. Transient Objects

The first hurdle I had to cross was figuring out who would manage my objects. At first, I thought that all objects would be managed by Coldspring, both in the service (UserService) and domain (User) layers. The UserService object would have behaviors, validate the data and pass it off to the User object that would do some other stuff and then pass over control to Transfer for saving the object to the database. You can see from this that I was a little lost as to how my objects were meant to interact. After taking the time to read Coldspring's Quickstart guide, however, I stumbled upon this section: Managing Singleton vs. Transient Objects with ColdSpring.

In a nutshell, the guide explains the difference between singleton objects and transfer objects, and details the role Coldspring has managing these objects. Singleton objects are objects that are unique within the application, and are often instantiated on application startup. Service layer objects can be considered singletons because they are each unique, and don't have any instance data: they serve as an interface between your controller and your domain object. A transient object, on the other hand, can be represented several times in your application. There can be several different versions of a User, but there will always ever be only one UserService.

Coldspring's job is to manage singleton objects, not transients. Therefore, in our example, it will only serve the UserService object. Who, then, takes care of our User object? In a world where we have limited amounts of RAM and server crashes are always a possibility, we need ways to constantly persist our data. Luckily, we have databases for that. Our transient objects will need to be saved to the database anyways, so why not just use Transfer to manage them for us? Transfer creates all the setX and getX methods we might need, and takes care of persisting our instances. All we need now is to get them to talk to each other.

Setting Up Coldspring and Transfer for our UserService and User Objects

The first thing we'll do is tell Transfer how our users table is setup.

sb.Users definition in the Transfer configuration file

view plain print about
1<package name='sb'>
2
3    <object name='Users' table='sb_users'>
4        <id name='id' type='string' />
5        <property name='username' type='string' />
6        <property name='password' type='string' />
7        <property name='email' type='string' />
8        <property name='roleid' type='string' />
9    </object>
10
11</package>

Now that Transfer knows about our users table, we can go ahead and configure Coldspring.

Coldspring configuration file

view plain print about
1<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
2
3<beans default-autowire='byName'>
4    
5    <!-- coldbox Factory -->
6    <bean id='ColdboxFactory' class='frameworks.coldbox_2_6_1.system.extras.ColdboxFactory' singleton='true'/>
7    
8    <!-- Datasource bean for Transfer -->
9    <bean id='softbreeze' factory-bean='ColdBoxFactory' factory-method='getDatasource'>
10        <constructor-arg name='alias'>
11            <value>${TransferDSNAlias}</value>
12        </constructor-arg>
13    </bean>
14    
15    <!-- Coldbox-transfer Config Factory -->
16    <bean id='TransferConfigFactory' class='frameworks.coldbox_2_6_1.system.extras.transfer.TransferConfigFactory' singleton='true' />
17    
18    <!-- Transfer configuration -->
19    <bean id='TransferFactory' class='frameworks.transfer_1_1.TransferFactory' singleton='true'>
20        <constructor-arg name='configuration'>
21        <bean factory-bean='TransferConfigFactory' factory-method='getTransferConfig'>
22            <!-- Config Path -->
23            <constructor-arg name='configPath'><value>${TransferConfigPath}</value></constructor-arg>
24            <!-- Definitions Path -->
25            <constructor-arg name='definitionPath'><value>${TransferDefinitionsPath}</value></constructor-arg>
26            <!-- ColdBox Datasource Bean -->
27            <constructor-arg name='dsnBean'><ref bean='softbreeze' /></constructor-arg>
28        </bean>
29        </constructor-arg>
30    </bean>
31    <bean id='Transfer' factory-bean='TransferFactory' factory-method='getTransfer' />
32    
33    <!-- softbreeze beans -->
34    <bean id='userService' class='softbreeze.model.userService' />
35
36</beans>

It may look like alot, but most of this is simply getting Coldspring to setup a Transfer bean for use in Coldbox. The last bean, userService, references our actual cfc file. A really neat feature of Coldspring is it's autowire functionality. What this does is look in each object and search for setX methods. For example, consider a setTransfer method in our userService object.

The userService's setTransfer method

view plain print about
1<cffunction name='setTransfer' access='public' returntype='void'>
2    <cfargument name='transfer' type='any' required='true' />
3    
4    <cfset variables.transfer = arguments.transfer />
5
6</cffunction>

When Coldspring sees this method, it knows to pass in the Transfer bean to it. This way, dependant beans are automagically inserted where needed (you could also do this using Coldspring's constructor-args, but for simple situations why not use simple solutions?). Now, how do we get all of this to work together? Let's imagine that our user is registering on our site. The event requested turns around and asks the userService object to register the new user. The userService object then validates the information provided and if everything is OK it then updates the user instance through Transfer. Let's go through that again step by step.

The event handler asks the userService objet to register the user

view plain print about
1<cffunction name='doRegistration' access='public' returntype='void'>
2    <cfargument name='event' type='any' required='true' />
3
4    <cfset var locals = { rc = event.getCollection() } />
5    
6    <!---
7        The ioc variable in our request collection is Coldbox's Coldspring plugin.
8     --->

9    <cfset locals.userService = locals.rc.ioc.getBean( 'userService' ) />
10
11    <cfset locals.rc.saveResult = locals.userService.register( arguments.event ) />
12
13    <!--- Show the view or run another event. --->
14
15</cffunction>

The controller doesn't really have a lot to do. It gets the userService bean (object) from Coldspring and then asks it to register the new user.

The userService object's register method

view plain print about
1<cffunction name='register' access='public' returntype='struct'>
2    <cfargument name='event' type='any' required='true' />
3
4    <!---
5        Prepare some local variables.
6     --->

7    <cfset var locals = {
8            result = {
9                    success = false, errors = { }
10                },
11            mandatory_fields = [ 'username', 'password', 'password_confirm', 'email' ]
12        } /
>

13
14    <!---
15        Loop over the mandatory_fields variable, and validate that they are not empty.
16     --->

17    <cfloop array='#locals.mandatory_fields#' index='locals.field'>
18        <cfif not len( event.getValue( locals.field, '' ) )>
19            <cfset locals.result.errors[ locals.field ] = 'MANDATORY_FIELD' />
20        </cfif>
21    </cfloop>
22
23    <!---
24        If the password_confirm field isn't empty, it must be the same as the
25        password field.
26     --->

27    <cfif len( event.getValue( 'password_confirm', '' ) ) and event.getValue( 'password', '' ) neq event.getValue( 'password_confirm', '' )>
28        <cfset locals.result.errors[ 'password_confirm' ] = 'DOESN_T_MATCH_PASSWORD' />
29    </cfif>
30
31    <!---
32        If there are no errors save the user and report a success.
33     --->

34    <cfif structIsEmpty( locals.result.errors )>
35
36        <!---
37            Create a new user instance from transfer.
38         --->

39        <cfset locals.newUser = variables.transfer.new( 'sb.Users' ) />
40
41        <cfset locals.newUser.setID( createUUID() ) />
42        <cfset locals.newUser.setUsername( event.getValue( 'username' ) ) />
43        <cfset locals.newUser.setEmail( event.getValue( 'email' ) ) />
44        <cfset locals.newUser.setPassword( hash( event.getValue( 'password' ), 'SHA-256' ) ) />
45
46        <cfset variables.transfer.save( locals.newUser ) />
47
48        <cfset locals.result.success = true />
49    </cfif>
50
51    <cfreturn locals.result />
52</cffunction>

There seems to be a lot going on here, but there really isn't that much. I start by looping over my mandatory_fields array to check for missing input data. Then, I make sure the password_confirm field matches with the password field (if it isn't empty). If all is good (no errors), we ask Transfer for a new user, and we set it's values using the automatically created setX methods. When all updates are done, we can turn around again and ask Transfer to save our newly created user. When the method is done, we return our results variable. With it, the controller can decide whether to show a message confirming registration, or display the form again with the errors provided.

And there you have it. This obviously doesn't represent a complete registration form, but it does show how to integrate Coldspring and Transfer together with Coldbox. I'm still not 100% sure this is best practice yet, but I am starting to feel more and more comfortable with this way of doing things. There is little to no link between the controller and the domain object, and all the interaction is handled through the service layer.

Any thoughts?