Organizing code can be a challenge for even the smallest teams. Frameworks help us solve this problem by establishing conventions that bind us to expected behavior. This contract can greatly improve the quality and efficencity of your product/service. While there are many approaches to code organization (and frameworks), I most appreciate the ColdBox Framework. If you are not familure with ColdBox, it is CFML framework and toolkit that supports Adobe's propriatary ColdFusion CFML engine and two open source CFML engines Railo and OpenBD. ColdBox is an event-driven framework that offers 16 conventional interception points while supporting an infinite number of custom interception points you declare.
A ColdBox interception point allows a developer to encapsulate code in a single CFC that has methods that will be automatically executed at a specific moment in the request. For example, you can check to see if a user's subscription is active before every request and encapsulate the code into a '/interceptors/UserSubscriptionInterceptor.cfc'. You may then use the same file to penalize 30 days of service if a user is browsing using IE6. Now, all your User Subscription code is encapsulated in a single CFC regardless of when it needs to be executed. Furthermore, ColdBox implicitly calls your methods so long as you have registered the interceptor in the configuration file.
Real-World Example
I often make use of Interceptors to encapsulate code into an 'Application Constructor Interceptor' that ensures application settings are available for each request. The following examples load information into my application cache that rarely changes using two conventional interception points: 'afterConfigurationLoad' and 'preProcess'.
The Related Files in My Application
The following files are used in this technique:
- /config/ColdBox.cfc
- Registers the interceptor with ColdBox.
- /interceptors/Constructor.cfc
- The actual interceptor.
- /model/DAO/ApplicationDAO.cfc
- Responsible for fetching registry data from the database.
- /model/objects/Registry.cfc
- The Registry object.
- /model/services/ApplicationService.cfc
- Provides the API to fetch the registry.
The Constructor Interceptor
The 'afterConfigurationLoad' method is automatically executed by the ColdBox framework after the application's configuration file has been consumed by the framework but before other services of the framework are available (such as logging). At this point, I can fetch my application's registry from the database and persist in memory for quick access throughout the application (including other initialization tasks the application may require).
<cffunction name="afterConfigurationLoad" returntype="void"
hint="Initializes the GreenleeBlog application's registry.">
<cfargument name="Event" />
<cfargument name="interceptData" />
<cfscript>
/* Construct the application registry */
var data = getModel('ApplicationService').generateRegistry();
getColdboxOCM().set('registry', data, getSetting('CacheApplicationTime'));
</cfscript>
</cffunction>In the above example, I am not using the CFML Application Scope and have opted for the ColdBox Cache Manager to persist the data. As such, my data may not always exist. Furthermore, I don't know how long the data will persist since the value was defined in the application's configuration file. To ensure the registry data is avalible during this request I will allays confirm the existence of the 'registry' and reconstruct (if needed) at the 'preProcess' interception point.
<cffunction name="preProcess" returntype="void"
hint="Reloads the GreenleeBlog application if cache expired. Assigns a reference in private request collection for registry.">
<cfargument name="Event" />
<cfargument name="interceptData" />
<cfscript>
/* Use the private request collection for data not directly relevant to the visitor */
var prc = event.getCollection( private = true );
/* Construct application registry if the cached version expired */
if (!getColdboxOCM().lookup('registry'))
getColdboxOCM().set('registry', getModel('ApplicationService').generateRegistry(), getSetting('CacheApplicationTime'));
/* Provide reference to registry */
prc.registry = getColdboxOCM().get('registry');
</cfscript>
</cffunction>Depending on your application's requirements, you may not need to construct any data at the 'afterConfigurationLoad' interception point and can rely exclusively on the 'preProcess' interception point. For example, the keywords (or tags) in my blog application can only change when a new post is added. So, the following example persists two forms of keyword data in my cache: all keywords and top keywords. The first key 'keywords' is an array of all keywords (objects), sorted alphabetically. The second key, 'top_keywords', is an array of my most frequently used keywords (objects again) sorted in descending order. Some of the meta-keyword data in the head of this page expects this second 'top_keywords' array to exist. Both of these keys are required by my layout and possibly by any Events in the framework, so the code is encapsulated into my Constructor Interceptor and my 'preProcess' execution point.
<cffunction name="preProcess" returntype="void"
hint="Reloads the GreenleeBlog application if cache expired. Assignes a reference in private request collection for registry and keywords.">
<cfargument name="Event" />
<cfargument name="interceptData" />
<cfscript>
/* Use the private request collection for data not directly relevant to the visitor */
var prc = event.getCollection( private = true );
/* Construct application registry if the cached version expired */
if (!getColdboxOCM().lookup('registry'))
getColdboxOCM().set('registry', getModel('ApplicationService').generateRegistry(), getSetting('CacheApplicationTime'));
/* Provide reference to registry */
prc.registry = getColdboxOCM().get('registry');
/* Construct keyword reference if the cached version expired */
if (!getColdBoxOCM().lookup('keywords'))
getColdboxOCM().set('keywords', getModel('BlogService').fetchKeywords(), getSetting('CacheApplicationTime'));
prc.keywords = getColdboxOCM().get('keywords');
/* Construct top keywords reference if the cached version expired */
if (!getColdBoxOCM().lookup('top_keywords') OR !getColdBoxOCM().lookup('top_keywords_list')) {
var kws = getModel('BlogService').fetchKeywords(sortByQuantity=true,max=getSetting('MaxTopKeywords'));
var i = 0;
var topkw = [];
for (i=1; i <= arrayLen(kws); i++)
arrayAppend(topkw, kws[i].getKeyword());
getColdboxOCM().set('top_keywords', arrayToList(topkw, ', ') , getSetting('RSSFeedCacheTime'));
}
prc.topKeywords = getColdboxOCM().get('top_keywords');
</cfscript>
</cffunction>Following is the complete Constructor Interceptor code.
<cfcomponent extends="coldbox.system.Interceptor" output="false"
hint="Application constructor used to persist slow data into memory.">
<cffunction name="afterConfigurationLoad" returntype="void"
hint="Initializes the GreenleeBlog application's registry.">
<cfargument name="Event" />
<cfargument name="interceptData" />
<cfscript>
/* Construct the application registry */
var data = getModel('ApplicationService').generateRegistry();
getColdboxOCM().set('registry', data, getSetting('CacheApplicationTime'));
</cfscript>
</cffunction>
<cffunction name="preProcess" returntype="void"
hint="Reloads the GreenleeBlog application if cache expired. Assigns a reference in private request collection for registry and keywords.">
<cfargument name="Event" />
<cfargument name="interceptData" />
<cfscript>
/* Use the private request collection for data not directly relevant to the visitor */
var prc = event.getCollection( private = true );
/* Construct application registry if the cached version expired */
if (!getColdboxOCM().lookup('registry'))
getColdboxOCM().set('registry', getModel('ApplicationService').generateRegistry(), getSetting('CacheApplicationTime'));
/* Provide reference to registry */
prc.registry = getColdboxOCM().get('registry');
/* Construct keyword reference if the cached version expired */
if (!getColdBoxOCM().lookup('keywords'))
getColdboxOCM().set('keywords', getModel('BlogService').fetchKeywords(), getSetting('CacheApplicationTime'));
prc.keywords = getColdboxOCM().get('keywords');
/* Construct top keywords reference if the cached version expired */
if (!getColdBoxOCM().lookup('top_keywords') OR !getColdBoxOCM().lookup('top_keywords_list')) {
var kws = getModel('BlogService').fetchKeywords(sortByQuantity=true,max=getSetting('MaxTopKeywords'));
var i = 0;
var topkw = [];
for (i=1; i <= arrayLen(kws); i++)
arrayAppend(topkw, kws[i].getKeyword());
getColdboxOCM().set('top_keywords', arrayToList(topkw, ', ') , getSetting('RSSFeedCacheTime'));
}
prc.topKeywords = getColdboxOCM().get('top_keywords');
</cfscript>
</cffunction>
</cfcomponent>Related Files
Today's post will not go into the service layer in debth, but, I wanted to share the service and DAO to provide a complete view of my solution.
The database schema is very simple and only includes three columns: ID, REFERENCE and PARENT.
<cfcomponent name="Registry"
hint="Application registry object for the Application.">
<!--- Data --->
<cfproperty name="values" type="string" />
<cfscript>
/* Construct */
function init() {
variables.values = { reference = {}, id = {} };
return this;
}
function setValue(string reference, numeric id, numeric parent) {
variables.values.reference[arguments.reference] = { id = arguments.id, parent = arguments.parent};
variables.values.id[arguments.id] = { id = arguments.reference, parent = arguments.parent};
}
function getValue(any key) {
if (isNumeric(arguments.key))
return variables.values.id[ arguments.key ];
else
return variables.values.reference[ arguments.key ];
}
function getValues(string method) {
if (arguments.method == 'id')
return variables.values.id;
else
return variables.values.reference;
}
function exists(any key) {
return structKeyExists(variables.values.id, arguments.key) && structKeyExists(variables.values.reference, arguments.key);
}
</cfscript>
</cfcomponent><cfcomponent name="ApplicationService" autowire="true" hint="Supports the construction of the application.">
<!--- AUTOWIRE PROPERTIES TO BE INJECTED BY COLDBOX --->
<cfproperty name="DatasourceBean"
type="coldbox:datasource:applicationData"
scope="variables.dependency" />
<cfproperty name="query_cache"
type="coldbox:setting:query_cache"
scope="variables.dependency" />
<cfproperty name="BeanFactory"
type="coldbox:plugin:beanfactory"
scope="variables.dependency" />
<cfproperty name="ApplicationDAO"
type="model:ApplicationDAO"
scope="variables.dependency" />
<cffunction name="generateRegistry" access="public" hint="Returns a constructed Registry object.">
<cfset var Registry = createObject('component', 'model.objects.Registry').init() />
<cfreturn variables.dependency.ApplicationDAO.fetchRegistry(Registry) />
</cffunction>
</cfcomponent><cfcomponent name="BlogDAO" autowire="true" hint="Returns properties that change infrequently. Useful during application construction.">
<!--- AUTOWIRE PROPERTIES TO BE INJECTED BY COLDBOX --->
<cfproperty name='DatasourceBean'
type='coldbox:datasource:applicationData'
scope='variables.dependency' />
<cfproperty name="BeanFactory"
type="coldbox:plugin:beanfactory"
scope="variables.dependency" />
<!--- CONSTRUCTOR --->
<cffunction name="init" access="public">
<cfreturn this />
</cffunction>
<!--- PUBLIC --->
<cffunction name="fetchRegistry" access="public" returntype="struct"
hint="Reads the registry and returns a struct of keys and a struct of ids to allow two methods of accessing the data.">
<cfargument name="Registry" hint="The application's registry object." />
<cfset var q = queryNew('') />
<cfquery name="q" datasource="#variables.dependency.DatasourceBean.getName()#"
username="" password="">
SELECT id, reference, parent FROM registry
</cfquery>
<!--- Populate Registry --->
<cfloop query="q">
<cfset Registry.setValue(reference = q.reference, id = q.id, parent = q.parent) />
</cfloop>
<cfreturn Registry />
</cffunction>
</cfcomponent>
