Re-factoring CFML Applications - Part 5 - Splitting up Your Code
In Part 5 of the series on re-factoring CFML applications, I go into specific details of how you might split up your existing code in order for it to fit in your new FW/1 application. The biggest single change in re-factoring from an old style CFML application into FW/1 is the requirement to properly split your code into the component parts that FW/1 expects to live in specific areas. Let’s take an example of a page that displays a set of tasks from a database. The original code might look like this:
<cfparam name="url.userID" default="0"/>
<!--- get some tasks for the user --->
<cfquery name="getTasks" datasource="mydsn">
SELECT task, taskID, dateCreated, isComplete
FROM tasks
WHERE userID = <cfqueryparam cfsqltype="cf_sql_integer" value="#url.userID#"/>
</cfquery>
<!--- display the tasks --->
<table>
<tr>
<th>Task</th>
<th>Created</th>
<th>Complete?</th>
</tr>
<cfoutput query="getTasks">
<tr>
<td><input type="text" name="task" value="#task#"/></td>
<td>#dateCreated#</td>
<td><input type="checkbox" value="#taskID#"<cfif isComplete> checked</cfif>>></td>
</tr>
</cfoutput>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes">
</td>
</tr>
</table>
Short and sweet. There is a good reason that we wrote so many applications like this early on in the days of the Internet, but it just isn’t sustainable in large applications over time, as we have learned. Let’s cut up this page into its component parts in FW/1.
Controller Code
Your controller component will have a few lines of code for this page, as you can see. The link in the browser might look something like this: /index.cfm?action=tasks.getTasks&userID=5, or, using SES URLs, /tasks/getTasks/userID/5. Note that FW/1 pulls URL and form variables into the rc scope for your convenience.
controllers/tasks.cfc
component accessors="true"{
property taskService;
public void function getTasks( rc ){
param name="rc.userID" default="0";
rc.tasks = taskService.getTasksByUser( userID = rc.userID );
}
}
Model Code
The inline query from the original page gets moved into the model and becomes available from anywhere in the application. I like to use a service layer fronting gateways and DAOs. A lot of methods in the service layer will be simple pass-through calls to gateways and DAOs, and that’s OK. It provides a place for logic that doesn’t belong in the controller. When you need that place, you’ll be thankful for it. Your mileage may vary. Note that FW/1 understands by convention that the file at /model/services/task.cfc is the taskService.
model/services/task.cfc
component accessors="true"{
property taskGateway;
property taskDAO;
public query function getTasksByUser( userID ){
return taskGateway.getTasksByUser( userID = arguments.userID );
}
}
model/gateway/task.cfc
component{
public query function getTasksByUser( userID ){
return queryExecute(
sql: "SELECT task, taskID, dateCreated, isComplete
FROM tasks
WHERE userID = :userID",
params: {userID: { CFSQLType: 'cf_sql_integer', value: arguments.userID }},
options: { datasource: variables.dsn }
);
}
}
View Code
In FW/1, views are simple .cfm files that have access to the rc scope where variables from the request and any data added from the controllers will be contained. In this example, all that has changed is that the query getTasks now lives in the rc scope, so we just need to reference it as rc.getTasks. There are places where you might need to have some CFML code in the view, but for the most part you should consider moving application logic into the controllers or the model as appropriate.
<!--- display the tasks --->
<table>
<tr>
<th>Task</th>
<th>Created</th>
<th>Complete?</th>
</tr>
<cfoutput query="rc.getTasks">
<tr>
<td><input type="text" name="task" value="#task#"/></td>
<td>#dateCreated#</td>
<td><input type="checkbox" value="#taskID#"<cfif isComplete> checked</cfif>>></td>
</tr>
</cfoutput>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes">
</td>
</tr>
</table>
End Results
The first thing a lot of programmers who are used to doing things the old way in CFML tend to notice is that there is a fair amount of extra code in the new solution compared to the old solution, and the inevitable question arises- wasn’t a goal to reduce the amount of code in the application?
Well, sort of. One goal is to reduce the overall amount of code, yes, but we accomplish that by reducing overlapping and duplicated code across the application. That requires a structured approach like FW/1 uses, and that approach requires a bit of extra code to break up functionality and encapsulate it where it can be accessed from across the application. In this example, the getTasksByUser method of the taskService is now available anywhere the taskService is available, so if that data is needed anywhere else in the application, we can access it with a one line call, taskService.getTasksByUser( userID ). In a large application, you will probably find dozens of overlapping and duplicate database calls for common database entities.
Something else that you will notice over time as you convert more code into your FW/1 application is that breaking up your code into smaller chunks makes it easier to find mistakes and debug them.
The above provides a very simple example of splitting up a page into multiple parts as FW/1 expects. It should be noted that there are far more complex examples, but the principles remain the same. In general, controllers contain calls to the model and other services, and application logic dealing with those calls. The model contains code that interacts with your database and other persistence layers, as well as other services like cloud storage, external API calls, etc. Views contain just that- the view of the application that the user sees.
Leaving Breadcrumbs
Something that I have thought recently about is the utility of leaving behind breadcrumbs in your existing code to note where that bit of code lives in the FW/1 application. Keeping track of where things have moved to becomes complicated over time, and being able to refer back to your notes will help you remember where things live in the new application and make your life easier when you come across duplicated code in another file and you need to figure out what can be de-duplicated. A few notes in your code might be enough. Can you think of a more thorough way to set your breadcrumbs? I would love to hear about it, let me know.
<cfparam name="url.userID" default="0"/>
<!--- /controllers/tasks.cfc/getTasks --->
<!--- /model/services/task.cfc/getTasksByUser --->
<!--- /model/gateway/task.cfc/getTasksByUser --->
<!--- get some tasks for the user --->
<cfquery name="getTasks" datasource="mydsn">
SELECT task, taskID, dateCreated, isComplete
FROM tasks
WHERE userID = <cfqueryparam cfsqltype="cf_sql_integer" value="#url.userID#"/>
</cfquery>
<!--- /views/tasks/gettasks.cfm --->
<!--- display the tasks --->
<table>
<tr>
<th>Task</th>
<th>Created</th>
<th>Complete?</th>
</tr>
<cfoutput query="getTasks">
<tr>
<td><input type="text" name="task" value="#task#"/></td>
<td>#dateCreated#</td>
<td><input type="checkbox" value="#taskID#"<cfif isComplete> checked</cfif>>></td>
</tr>
</cfoutput>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes">
</td>
</tr>
</table>