Using Cached Components As Scoped Proxies In ColdFusion
One of my secondary reasons for building Big Sexy Poems is that it gives me a real-world context in which to try out new (to me) programming paradigms. And one approach that I've been rather enjoying is the use of cached ColdFusion components as Scoped Proxies. This provides the benefit of request-specific state without the overhead and divergent mechanics of per-request component instantiation.
Big Sexy Poems, like all of my ColdFusion applications, uses Inversion of Control (IoC) with a Dependency Injection (DI) container that manages the caching and wiring-up of ColdFusion components (.cfc). My Injector.cfc is a comparatively lightweight, no-frills DI implementation that deals exclusively with long-lived components. Meaning, it has no mechanics for transient, short-lived instantiations.
For some types of request-specific state, this long-lived nature isn't a problem. For example, an abstraction that deals with cgi scope access or that simplifies HTTP header reading can proxy the low-level request metadata without worrying about state since said state is readonly.
A RequestMetadata.cfc component that provides an abstraction over the cgi scope is request-specific but not dynamic in a meaningful way:
component {
/**
* I return the ETag for the given request (or the empty string if none exists).
*/
public string function getETag() {
return cgi.http_if_none_match;
}
/**
* I return the HTTP host.
*/
public string function getHost() {
return cgi.server_name;
}
}
But some ColdFusion components are both request-specific and dynamic. Take my Router.cfc for example. Part of the router abstraction is a set of methods that work together to "walk" the dot-delimited event parameter, making it easier for the request to invoke the targeted CFML modules.
Here is a truncated version of my root controller. Notice that it calls router.next() within the switch expression and then router.nextTemplate() within the case body:
<cfscript>
switch ( router.next() ) {
case "account":
case "auth":
case "dev":
case "go":
case "marketing":
case "member":
case "share":
case "system":
cfmodule( template = router.nextTemplate() );
break;
}
</cfscript>
The .next() method is shifting the next dot-delimited segment off of the event parameter and returning it to the controller. But, it's also setting an internal state variable that's subsequently consumed in the .nextTemplate() call. To illustrate, for the event parameter:
member.poem.list
... the .next() call would return the next segment:
member
... and the subsequent .nextTemplate() call would return the next CFML module path:
./member/member.cfm
Since this state is both updated and consumed several times within a single request (for nested modules), it has to be initialized once at the top of each request. And for that, these type of cached scoped proxies define a special method: setupRequest().
At the root router of my ColdFusion application, I have to gather all of the relevant scoped proxies and initialize them. To see this more clearly, here's my less truncated root router. You will see that I'm calling .setupRequest() on three different ColdFusion components.
<cfscript>
// Define properties for dependency-injection.
requestHelper = request.ioc.get( "core.lib.web.RequestHelper" );
requestMetadata = request.ioc.get( "core.lib.web.RequestMetadata" );
router = request.ioc.get( "core.lib.web.Router" );
// ColdFusion language extensions (global functions).
include "/core/cfmlx.cfm";
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// While these components are all cached in the application scope, they all need to
// operate, in part, on request-scoped variables. As such, we have to initialize the
// request-scoped variables at the start of each request.
requestMetadata.setupRequest();
requestHelper.setupRequest();
router.setupRequest();
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
switch ( router.next( "marketing" ) ) {
case "account":
case "auth":
case "dev":
case "go":
case "marketing":
case "member":
case "share":
case "system":
cfmodule( template = router.nextTemplate() );
break;
default:
throw( type = "App.Routing.InvalidEvent" );
break;
}
</cfscript>
It's important to remember that while I'm setting up request-specific data, the cached ColdFusion component instances are still shared across all requests. Which means that I can't use to the variables scope internally, the way I would with a transient component. Instead, I have to use the request scope to store request-specific state; but, I've settled on creating an internal "variables abstraction" — $variables() — that proxies the request scope in an effort to telegraph my intent of "member state".
To illustrate, here's a truncated version of my Router.cfc. Note that the setupRequest() method initializes the request.$$routerVariables structure for the given request; and then the $variables() method provides access to said structure:
component {
// Define properties for dependency-injection.
property name="requestMetadata" ioc:type="core.lib.web.RequestMetadata";
// ColdFusion language extensions (global functions).
include "/core/cfmlx.cfm";
// ---
// LIFE-CYCLE METHODS.
// ---
/**
* I set up the core request structure used internally by the router.
*/
public any function setupRequest( string scriptName = "/index.cfm" ) {
var event = listToArray( url?.event, "." );
request.$$routerVariables = {
scriptName: scriptName,
event: event,
queue: duplicate( event ),
currentSegment: "",
persistedSearchParams: []
};
return this;
}
// ---
// PUBLIC METHODS.
// ---
/**
* I return the next event segment, or the given fallback if there is no next segment.
*/
public string function next( string fallback = "" ) {
var segment = $variables().currentSegment = $variables().queue.isDefined( 1 )
? $variables().queue.shift()
: fallback
;
return segment;
}
/**
* I return the next relative template path used by the view routing.
*/
public string function nextTemplate( boolean nested = true ) {
return nested
? "./#segment()#/#segment()#.cfm"
: "./#segment()#.cfm"
;
}
/**
* I return the current event segment as of the latest route traversal.
*/
public string function segment() {
return $variables().currentSegment;
}
// ---
// PRIVATE METHODS.
// ---
/**
* I am a convenience method to access internal variables scoped to the request.
*/
private struct function $variables() {
return request.$$routerVariables;
}
}
The use of $variables() isn't strictly necessary — I could have directly referenced the request.$$routerVariables struct throughout the component. But, I use the $variables() method as a point of indirection to decouple the underlying key; and as a means to express the intent of mimicking a more traditional variable scope. Essentially, I'm saying to the developer (future me) that this bag of data is the "variables scope" but tied to the current request.
The huge benefit of this approach is that I can maintain uniformity in my dependency injection — it's nothing but long-lived components. This simplifies the mechanics of the application's inversion of control without giving up the very real need to have request-scoped state.
Want to use code from this post? Check out the license.
Reader Comments
At first, I thought maybe this was the "Flyweight Pattern"; which I was excited about because I don't think I've ever used it by name. But I asked Claude Code if this matches the pattern and it gave me some nuance:
So it was close - same ballpark of design patterns; but, just a little different.
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →