ColdFusion MX Coding Guidelines - Structure: Application,
Component, Tag etc
Release 3.0.2 (10/17/2003)
« Style: Naming, Comments
& Layout | Contents | Good
Practice »
Structure: Application, Component, Tag etc
This section provides guidelines on how to structure your code
and take advantage of the power of ColdFusion Components, to create well-designed,
maintainable ColdFusion applications.
In general, MVC - Model-View-Controller - is a good, basic design pattern
to use as a guideline for designing your application. It helps you focus on
separating logic from presentation as well as refining the logic to separate
the pure business model from the application workflow and logic.
Construct as much of the application logic as possible using CFCs so that
you can take advantage of the encapsulation and type safety that they offer,
as well as providing better options for reuse. Structure the CFCs to be as
independent of each other as possible and as self-contained as possible (loose
coupling and high coherence respectively). In particular, structure CFCs so
that environmental awareness (e.g., use of shared scopes) is minimized, using
design patterns such as Session Façade.
Mach II provides a well-designed,
clean framework for building MVC-based applications. See the Mach
II Development Guide for more information about object-oriented design
and best practices for using that framework.
Basic Modularity
ColdFusion components, custom tags, tag libraries and included files should
be used if their usage will satisfy any of the following three conditions:
- Reusability
- Readability
- Organization
That means that all but the very simplest ColdFusion page should take advantage
of CFCs and / or custom tags. Components should be used in preference
to custom tags (for encapsulation and type safety reasons) although in certain
situations, e.g., where part of a page has a natural start and end that needs
to be managed as a single unit, custom tags can be more idiomatic.
A good example
is the use of mmlf:renderpage
(and mmlf:rnav
)
to wrap the body (and right navigation module) of a page and render it using
standard header, footer
and style sheets. Another good example of when it is natural to use a custom
tag is the mmlf:ssi
tag, used to perform 'server-side includes'
of HTML fragments from the web servers. Both of these uses would be harder
to achieve with CFCs and would be less intuitive to use.
If performance
is
critical
or
external
integration
requires
it, Java tag libraries should be used with cfimport
in preference
to CFX tags.
File Structure
Each file should begin with an appropriate comment - see Style:
Comments.
Component File Structure
CFCs should be structured as follows:
<!--- prolog comment --->
<cfcomponent ...>
...pseudo-constructor initialization (if any)...
...public methods (with init() first)...
...package methods (if any)...
...private methods...
</cfcomponent>
The use of pseudo-constructor initialization should be kept to a minimum and
instead an init()
method
should be used to initialize the component - see Good
Practice: Constructors. The public methods are
the most important part of the component so they the first thing someone reads.
The public methods should be followed by any access="package"
methods
and then any access="private"
methods. Users of a component
should not have to read as far as the private methods in order to figure out
how to use your component - well-chosen names (and good comments) for the public
methods should be sufficient.
.cfm
File Structure
Even within a single file, separate logic from presentation as much as possible.
If logic and presentation code cannot be physically separated
(into
different files),
then
try to structure
files along the following lines:
<!--- prolog comment --->
<cfsilent>
...CFML logic...
</cfsilent>
<cfoutput>
...HTML generation...
</cfoutput>
Note: cfsilent
suppresses all HTML
output. This should not be a problem if logic and presentation code are properly
separated. An Alternative is to use cfsetting
as follows:
<cfsetting enablecfoutputonly="yes" />
<!--- prolog comment --->
...CFML logic...
<cfoutput>
...HTML generation...
</cfoutput>
<cfsetting enablecfoutputonly="no" />
Note: You should have both the yes and the no versions of the tag present
and in the same file (to avoid creating hard-to-debug problems with unexpected
output or missing output).
Directory Structure For Applications
All ColdFusion code should live in a directory tree outside the install area
for ColdFusion MX / JRun. On most servers, the root for that directory tree
is /data/www/appserver/cfmx/
and we'll refer to that as the {cfmxroot}
below:
{cfmxroot}
wwwroot/ » web-accessible .cfm pages and .cfc Web Services
extensions/
components/ » tree for .cfc files
customtags/ » tree for .cfm custom tags
includes/ » tree for include files
config/ » tree for configuration files
This implies that we have two Custom Tag Paths set up in the CFMX Administrator:
{cfmxroot}/extensions/components/
{cfmxroot}/extensions/customtags/
We also have mappings for the root of the includes tree (for cfinclude
)
and the custom tags tree (for cfimport
):
/cfinclude » {cfmxroot}/extensions/includes/
/customtags » {cfmxroot}/extensions/customtags/
For Mach II development, we also have a mapping for including the core file:
/MachII » {cfmxroot}/extensions/components/MachII/
See Site-wide Variables for information about a /environment
mapping.
The pieces of each logical application live in an application-specific directory
in each of the trees above, e.g., code for the Exchange application lives
in:
{cfmxroot}/wwwroot/exchange/
{cfmxroot}/extensions/components/exchange/
{cfmxroot}/extensions/customtags/exchange/
{cfmxroot}/extensions/includes/exchange/
Any Java libraries required should live in
JRun's servers/lib/
directory
(although, perhaps a little confusingly, in our build system we still have ant
deploy to WEB-INF/lib/
as
if it were part of CFMX and then the build system moves the files to the
right place!).
URL Usage
URLs must not hardcode server names such as www-staging
, www.macromedia.com
,
etc. These variables will be different on staging, QA,
integration and production and should be handled using Site-wide Variables
(in the next section).
URLs form the API to our web site. We have to live with them forever. Take
the time to get them right, design your query string parameters carefully,
be consistent, etc. Here are some preliminary
specs.
Site-wide Variables
Some attributes within web applications depend on the server environment and
will differ between development, staging, integration and production. The recommended
approach for such attributes is to provide their values as request
scope variables that are set as part of
Application.cfm
. However, Application.cfm
itself should
be a deployable file that is independent of the server environment so the variables
should be set in a server-specific include file (i.e., a file that has the same
name but different content on every server). This way, Application.cfm
will be a standard, deployable source file that is identical in each of the
four environments while the included file, or database table contents, are considered
part of the server configuration itself.
The server-specific include file will be called sitewideconstants.cfm
and will exist in directories for development, staging, integration and production.
The root Application.cfm
will include the file as follows:
<cfinclude template="/environment/sitewideconstants.cfm">
In each environment, /environment
will be mapped to the appropriate
directory, outside the document root. For the most part, this is the target
config
directory created automatically by the build system ({cfmxroot}/config/target/
).
Similarly, /cfinclude
will
be mapped to the include file root ({cfmxroot}/extensions/includes/
).
The build system automatically creates a serverspecific.cfm
configuration
file that contains:
request.buildTag
- string identifying the build on this server
request.buildWebServer
- string identifying
the web server name for this back end system, e.g., "www.macromedia.com"
request.buildAppServer
- string identifying
this application server name, e.g., "d65app1.macromedia.com";
request.buildAppCluster
- string list identifying all the application server
names in this cluster, e.g.,
"d65app1.macromedia.com,d65app2.macromedia.com,d65app3.macromedia.com"
The primary Application.cfm
file will also include that, as follows:
<cfinclude template="/environment/serverspecific.cfm">
The CVS
source code control tree looks like this:
/source/
docroot/ Web Server Document Root
swf/ .swf
java/ Java Source Root
com/
macromedia/
eai/ com.macromedia.eai package
...
config/
development/ Development configuration for Java apps
application/
Specific configuration for application
staging/
application/
integration/
application/
production/
application/
neo_root/ Application Server Root
config/
development/ Development
staging/ Staging
integration/ Integration
production/ Production
target/ Deployed directory (/environment)
extensions/ Non-URL accessible CF files
components/ ColdFusion Components
MachII/ Mach II framework (/MachII)
customtags/ Custom Tags (/customtags)
includes/ Included Files (/cfinclude)
wwwroot/ .cfm Document Root
In addition to the source code tree shown above, the following directories
will also exist in the repository, which is documented in full in the Dylan65
CVS Layout.
source/
database/ DDL and other files
docs/ Engineering Document Tree
infrastructure/ Non-Web Configuration (e.g., messaging)
orientation/ Orientation Projects
qa/ QA (e.g., test harnesses)
release_eng/ Release Engineering (e.g., scripts)
scratch/ Scratchpad for anything!
The general assumption is that global include files of all sorts will live
outside the document root, in appropriate directory structures, with suitable
logical names for mappings.
Application.cfm
There will be a root Application.cfm
file that provides all the
site-wide core services such as application and session settings, site-wide
constants, form / URL encoding machinery etc.
Each "application" on the site will also have an Application.cfm
file containing application-specific code that starts by including the root
Application.cfm
file. Each "application" will also typically
have an include file, applicationvariables.cfm
, that defines
the application-specific variables. This will also be included by the application-specific
Application.cfm
file. The variables should be those that might
be needed by other applications that need to take advantage of the services
of this application, e.g., the membership application might define an include
file with LDAP and data source settings, for use by the store and exchange
applications.
The applicationvariables.cfm
file belongs in:
{cfmxroot}/extensions/includes/{appname}/
/Application.cfm:
- set up application and session settings:
<cfapplication name="macromedia_com" sessionmanagement="true"
...>
- user session setup (sets up
session.membership.user
etc).
- process
loc=
to create request scope language / locale values - see Globalization.
- globalization / encoding:
<!--- Set encoding to UTF-8. --->
<cfprocessingdirective pageencoding="utf-8">
<cfcontent
type="text/html; charset=UTF-8">
<cfset setEncoding("URL", "UTF-8")>
<cfset setEncoding("Form", "UTF-8")>
- include site-wide and server-specific constants:
<!--- Site-wide constants --->
<cfinclude template="/environment/sitewideconstants.cfm">
<!--- server-specific variables --->
<cfinclude template="/environment/serverspecific.cfm">
/{appname}/Application.cfm:
- include root file:
<cfinclude template="/Application.cfm">
- include public application-specific variables:
<cfinclude template="/cfinclude/{appname}/applicationvariables.cfm">
- set up private application-specific data
Exception Strategy
ColdFusion MX allows exceptions to have a pseudo-hierarchy by allowing cfcatch
to specify a type=
attribute that has a compound dot-separated
name, like a component name, that will catch exception that have that type or
a more specific type, e.g.,
<!--- catches feature.subfeature.item: --->
<cfcatch type="feature.subfeature.item">
<!--- catches feature.subfeature.{anything}: --->
<cfcatch type="feature.subfeature">
<!--- catches feature.{anything}: --->
<cfcatch type="feature">
Each 'application' (or feature) should define and publish its exception hierarchy.
Most hierarchies will probably only have feature and item,
e.g., Membership.NO_SUCH_ATTRIBUTE
. The intent is that the feature.item
(or feature.subfeature.item
) type should entirely specify what
exception has occurred.
Each application should throw fully-qualified exception types (feature.item
or feature.subfeature.item) and a descriptive message that says - in
English - which component / method threw the exception and a description of
the problem, e.g.,
<cfset msg = "MembershipAdminMgr.setAttributeName() :: " &
"no attribute could be found with the given attribute name">
<cfthrow type="Membership.NO_SUCH_ATTRIBUTE" message="#msg#">
The code that invokes that application should catch the exceptions using fully-qualified
types that it can handle, followed by feature.subfeature or feature
for reporting more generic exceptions, e.g.,
<cfcatch type="Membership.NO_SUCH_ATTRIBUTE">
<!--- handle missing attribute error --->
</cfcatch>
<cfcatch type="Membership">
<!--- handle general membership failure --->
</cfcatch>
<cfcatch type="application">
<!--- handle general application failure --->
</cfcatch>
...
Debugging
A page may accept a URL parameter debug=1
to enable debugging
output within the page. Such pages should use the getrequestsettings
tag
which processes URL parameters into request
scope. The appropriate sitewideconstants.cfm
file
controls whether or not debugging is enabled in a particular environment.
Debugging output within a page should be structured as follows:
if ( request.settings.debug ) {
// output debugging information
}
Do not use URL.debug
directly.
Shared Scopes, Sessions, Clustering & Locking
This section discusses the considerations behind use of shared scopes (application
,
server
, session
etc), how sessions are managed, how
we use clustering and what to do about locking.
Clustering & Session Scope
We use hardware load balancing with sticky session between our web
servers and our application servers. We use the underlying J2EE Session Variables
mechanism (which uses an in-memory, browser-based cookie). We rely on session
scope
data in many of our applications, including storing CFC instances in session
scope.
If a server drops and we lose session, that user will get switched automatically
to a new server in the cluster and we will have to recreate their session data.
This can impact two things:
- Authentication level (see below for more details),
- Session-specific data.
If a session is lost, "Level 2" membership applications will require
users to verify their login credentials again at that point ("Level 1" membership
applications will be unaffected - in terms of authentication - since the "Remember
Me" cookie determines that level of authentication).
Session-specific data needs a little more care:
- If the data is being cached
purely for performance reasons, then it can easily be recreated (e.g.,
the shopping cart in the store).
- If the data represents historical state in a session (such as data entered
in previous forms) then a judgment call should be made as to how important
it is to preserve the information (e.g., by passing all the
data between subsequent page requests rather than using
session
scope).
As a general guideline, use session
scope sparingly.
Use of Shared Scopes
Do not use client
scope. Client scope limits you to text data
(so structured data needs to be WDDX-transcoded in and out of client
scope);
it relies on either persistent cookies or database storage - the former is
intrusive for users (and doesn't work well for shared computers), the latter
introduces a potential performance hit on every request (and we try to keep
database access to a minimum).
You may use session
scope for user-specific data but see the caveats and considerations
above.
For data caching that is not user-specific, use server
scope or application
scope. For macromedia.com, we're the only application in town (because
we need a single session - single sign-on - across all parts of macromedia.com),
so we use server
scope instead of application
scope
(server
scope access is marginally faster than application
scope).
Locking & Shared Scopes
When accessing and / or updating data in shared scopes, always consider the
possibility of race conditions (i.e., where two concurrent requests could access
and / or update the same data). If a race condition is possible and might affect
the behavior of your code, you need to use cflock
to control execution
of the code around the shared resource.
If the shared resource is in server
or application
scope,
you should use named locks to control access - and choose a name that uniquely
identifies the resource being locked, e.g., use server_varname
when
locking an update to server.varname
.
If the shared resource is in session
scope, you should generally
use a scoped lock on session
itself.
In both cases, remember that you are only trying to prevent race conditions
affecting your code: most read operations do not need to be locked;
most write operations should be locked (unless the race condition
is either unimportant or cannot affect the outcome).
Authentication Levels & Sessions
macromedia.com has three levels of authentication:
- guest
- known, unauthenticated ("remembered")
- known, authenticated
Machinery exists in the root Application.cfm
that establishes
which state the current session is in and creates a session.membership.user
object that can be queried:
getAuthLevel()
- string - GUEST_USER
, REMEMBERED_USER
,
AUTHENTICATED_USER
getAuthLevelID()
- numeric - 0, 1, 2 respectively
isLoggedIn()
- boolean - true if level 2 else false
getUserID()
- numeric - internal user ID if level 1 or 2 else
-1
For more details, your can read the session
authentication spec.
« Style: Naming, Comments
& Layout | Contents | Good
Practice »
Source: http://livedocs.macromedia.com/wtg/public/coding_standards/structure.html