Collocating Views And View-Specific Components In ColdFusion
In web application development, there's generally two philosophies when it comes to organizing files: "separation of concerns" and "collocation of behaviors". In the ColdFusion world, the pendulum or organization has swung from the collocation of behaviors—in the early days—to more of a separation of concerns within the modern MVC (Model View Controller) frameworks. But, I think the pendulum has swung too far over; and needs to return to the center where we can leverage both philosophies in the places that they make the most sense. To that end, I'll be experimenting with collocating my CFML views with the ColdFusion components that contain view-specific logic.
As I've gotten older, and I've learned a lot from my mistakes, I've come to understand that code organization is a major part of what makes an application maintainable over the long term. In that respect, one of the most important things that I can do, as an application author, is make my code easy to find and easy to delete. And, a great way to do that is to keep related files right next to each other in the file system.
I've recently started to collocate my CFML, JavaScript, and Less CSS files; and it's made some of my recent multi-page application (MPA) work much more enjoyable. The last step in that evolution is start collocating my view-specific ColdFusion components (CFCs) right next to the views that need them.
To be clear, I'm not saying that all CFCs should be right alongside the CFML views—many CFCs in a ColdFusion application are not view specific; and, shouldn't be mixed in with the CFML view files. But, many CFCs are view specific; and, shouldn't be mixed in with the "core" components.
It's helpful (to me) to think about a web application as having two macro responsibilities. There's the "application core" and the "delivery mechanism". The application core owns all of the core data models and workflows—the ColdFusion components that define the foundational meaning of the application. The delivery mechanism is the thin(ish) layer that sits on top of the application core and connects it to the outside world.
Aside: This terminology of the application core and the delivery mechanism is borrowed from Robert "Uncle Bob" Martin.
Drawing the boundary between these two responsibilities is not always easy; and it's an ongoing struggle for me. I don't pretend that this mental model is perfect—only that it provides me with a scaffolding on which to mold the application.
ColdFusion components that are view-specific and are not relating to the core application model, should not be in the "application core"—they should be in the "delivery mechanism". Where within the delivery mechanism depends on just how view-specific they are.
For example, a cross-cutting concern within the delivery mechanism, such as session cookie management, should be in a common location. But, when a component is responsible for aggregating data that is specific to a single view and is not used anywhere else within the application rendering, that ColdFusion component should be right next to the view that consumes it.
To explore this concept, let's imagine that I have a "Demo" feature. This demo feature can be a composite of several files that are collocated in the file system:
demo.cfm
- this is the "controller" that takes the request, wrangles the data, invokes operations on the "application core", and renders the view.demo.view.cfm
- this is the "view" rendered by the controller.demo.view.js
- this is the view-specific JavaScript.demo.view.less
- this is the view-specific CSS.demoPartial.cfc
- this is the ColdFusion component that provides view-specific data aggregations and transformations.demoPartialGateway.cfc
- this is the ColdFusion component that executes bespoke SQL queries that pertain to the view-specific rendering needs.
Aside: I would have loved to have named the CFCs something like
demo.partial.cfc
, but putting dots in a ColdFusion component filename is problematic due to the way in which the dot-paths work in ColdFusion component resolution.
To see this in action, here's a sample of what the demo.cfc
controller might look like. This code uses my Dependency Inject (DI) component for Inversion of Control (IoC):
<cfscript>
// This application view has data requirements that are specific to this view and will
// not be used by any other view in the application. As such, I think the CFCs
// relating to the data access and aggregation should live RIGHT HERE alongside the
// view files.
demoPartial = request.ioc.get( "scribble.cfc-test.demoPartial" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
viewModel = demoPartial.getViewModel( userID = 1 );
message = viewModel.message;
dbData = viewModel.dbData;
hitCount = viewModel.hitCount;
// Render the view.
include "./demo.view.cfm";
</cfscript>
In this case, scribble.cfc-test
is the dot-delimited directory path in which both the demo.cfm
and the demoPartial.cfc
live. The demo.cfm
file is deferring to the demoPartial.cfc
ColdFusion component for the data loading. The demoPartial.cfc
is also using my IoC / DI component to gain access to the gateway component:
component
output = false
hint = "I provide VIEW-SPECIFIC data aggregation methods."
{
// Define properties for dependency injection.
property name="gateway" ioc:type="scribble.cfc-test.demoPartialGateway";
/**
* I initialize the view component.
*/
public void function init() {
variables.hitCount = 0;
}
// ---
// PUBLIC METHODS.
// ---
/**
* I return the view model for the partial.
*/
public struct function getViewModel( required numeric userID ) {
var dbData = gateway.getDataFromDatabase( userID );
return {
message: "Hello for demo view!",
dbData: dbData,
// Caution: this is NOT THREAD SAFE logic, I'm just using it as a demo.
hitCount: ++hitCount
};
}
}
And the demoPartialGateway.cfc
component is really just needed to simplify the SQL execution:
component
output = false
hint = "I provide VIEW-SPECIFIC data access methods."
{
/**
* I get some data from the database that is only ever used for this view.
*/
public array function getDataFromDatabase( required numeric userID ) {
return queryExecute(
"
SELECT
( :userID ) AS id,
( 'foo' ) AS bar,
( 'hello ' ) AS world
;
",
{
userID: userID
},
{
returnType: "array"
}
);
}
}
Now that all of the view-specific files are side-by-side in the file system, making changes that affect several of the files is very easy. This code is very easy to find; and, very easy to delete should we decide that this feature no longer serves a purpose in the application.
Of course, not every view needs all of these files. Just as not every view needs view-specific JavaScript and CSS files, not every view needs a Partial component. In fact, even when I have some non-trivial data-aggregation requirements, I might just put those in the controller (see the "share controller" in my Incident Commander as an example.
As Dave Farley said, the best definition of "good code" is code that's "safe and easy to change." By collocating all of the view-specific files right next to each other, the file system itself is making assertions about the scope and relation of the files; which I believe makes the code safer and easier to change. Which in turn, makes the code "good".
Want to use code from this post? Check out the license.
Reader Comments
I 100% agree and try to co-locate my code whenever possible. In fact, if I have js/css that is applicable to a specific view, I just embed it in that view so that I don't even have to go to a second file. That's just my current preference, but I do a fair amount of equivocating on this.
Is the dot notation just preference
demo.partial.cfc
or is there another more practical reason for the naming convention? If it's just preference, maybe snake case would be worth consideringdemo-partial.cfc
anddemo-partial-gateway.cfc
for overall parity?@Chris,
The dot-notation was more about having different but similar files sort nicely in the file system. But, it causes a problem with the CFC since ColdFusion gets confused between the dots and the directory paths. To be honest, I'm not even sure if you can use a
-
in a CFC filename either? I'll have to try that. In which case, the-
would be a nice and consistent alternative to the.
.Ok, so it looks like you can use the
-
in the CFC file name as long as you are instantiating it with a quoted path. Meaning, given a CFC with the filenamemy-test.cfc
, you can either use:createObject( "component", "my-test" );
or, by quoting the
new
operator target:new "my-test"();
Given that information, the
-
might be a more reasonable token delimiter for grouping files. Something more like:Good food for thought!
@Ben Nadel,
Good to know! The
new "my-test"()
syntax though 😬. I never even would have thought to try that!The
createObject()
notation isn't as cringe.Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →