Release 1.2.1 (4/14/2005)
« Application Structure & Design | Contents | Designing Views »
This section looks at design issues in the Model portion of your application. The Model encompasses all of the business logic in your application and is implemented as a collection of ColdFusion Components.
Mach II interacts with the Model portion of your application using components
that extend MachII.framework.Listener. In a simple application,
all of your business logic might be implemented in such listeners but this approach
does not scale well with increasing application complexity because, amongst other
things, it introduces a tight coupling between the elements of your business
logic and the Mach II framework. If you've carefully designed your business components
based on the guidelines in the previous section, it should be clear that only
a few of those components are natural "listeners" for events. It might
even be that none of your basic business components are natural "listeners".
This is not a problem!
You are much better off ensuring there is only a thin layer of coupling between the framework and your application - if necessary, create new components that extend the Mach II listener component, whose sole purpose is to communicate those events to the components within your business model. In other words, try to isolate your business components from the framework components as much as possible: only listener components should know about the framework; only listener components should access Mach II properties; only listener components should announce events. The core business components should know nothing about Mach II and have no dependencies at all on the framework.
A minimal listener CFC that uses the CFCInvoker_Event invoker
type looks like this:
<cfcomponent extends="MachII.framework.Listener"> <cffunction name="configure" returntype="void" access="public" output="false"> <!--- perform any initialization ---> </cffunction> <cffunction name="someMethod" returntype="someType" access="public" output="false"> <cfargument name="event" type="MachII.framework.Event" required="yes" /> <!--- perform some task ---> <cfreturn someValue /> </cffunction> </cfcomponent>
The method, someMethod, can be invoked from an event
handler in mach-ii.xml using
the <notify> command. The returned value, someValue,
should be of type someType. Note that someType can
be void if the method does not return anything - either it does
not contain a <cfreturn /> tag or it specifies no expression
in <cfreturn />.
The listener is declared in mach-ii.xml as follows:
<listener name="listenerName" type="Path.To.YourListener"> <invoker type="MachII.framework.invokers.CFCInvoker_Event" /> </listener>
The <invoker> tag specifies how the methods should be invoked
on the listener. In general, you should specify CFCInvoker_Event as
shown above which will cause the current event object to be passed as a single
argument to the invoked method. You can also specify CFCInvoker_EventArgs which
will cause the current event's arguments to be passed individually as named
arguments into the method. See the section on Invokers
& Listeners that follows
for more detail.
You may optionally specify parameters for the listener declaration:
<listener name="listenerName" type="Path.To.YourListener"> <invoker type="MachII.framework.invokers.CFCInvoker_Event" /> <parameters> <parameter name="param1" value="value1" /> <parameter name="param2" value="value2" /> </parameters> </listener>
This provides default parameter values for param1 and param2 (of value1 and value2 respectively).
These can be accessed within the listener using the getParameter() method,
e.g., getParameter("param1") (the listener inherits this method from the Listener base class).
The methods of a listener CFC can be invoked in an event handler as follows:
<notify listener="listenerName" method="someMethod" resultKey="someVariable" />
This causes the someMethod() method to be called with
the current event and the returned result to be stored in someVariable,
e.g., request.result. The resultKey attribute is
optional (and should not be specified for methods that have returntype="void").
A listener has access only to the current event although it can announce additional
events to be added to the queue (and which will be executed at some point after the current event has been handled) using announceEvent().
A listener method typically does its job by calling one or more methods on
one or more business model objects. The business model objects might be created
on the fly, might be managed in session scope or might be created
within the listener's configure() (and stored in the listener's variables scope).
For more information on writing listeners, refer to the Mach II article How to... Develop Listeners by Ben Edwards.
When a listener is declared in mach-ii.xml, you have to specify
an invoker type. Mach II provides two default types:
CFCInvoker_EventArgsCFCInvoker_EventThe invoker affects how arguments are passed to the listener methods in a <notify> tag and what happens to the result.
As noted above, CFCInvoker_EventArgs causes
all the event arguments to be passed to the method as separate named arguments. CFCInvoker_Event causes
the whole event object to be passed as a single argument (of type MachII.framework.Event)
to the method. Both invokers store the result in the specified variable, which means you have to use a request scope variable. To some extent, which you use is a matter of style but there
are some practical considerations:
CFCInvoker_EventArgs may seem to provide better type safety
since you can specify the type of each argument (and whether it is required
and, if not, whether it should have a default) but letting ColdFusion itself
validate what is really your URL and form data is not really very robust!CFCInvoker_Event provides more flexibility at the expense
of requiring additional code inside your listener method to access and validate
the event arguments.CFCInvoker_Event and consider using event
filters to enhance the type safety of your application. If you use CFCInvoker_Event then your listener methods are passed a single argument which should be declared as follows:
<cfargument name="event" type="MachII.framework.Event" required="true"/>
You can test whether a given URL / form variable was provided using the isArgDefined() method on the event:
<cfif arguments.event.isArgDefined("anArg")>
...
</cfif>
Since URL / form variables are inherently strings (as far as HTTP is concerned), you are better off explicitly validating the event argument types in your own code, rather than trying to use CFCInvoker_EventArgs and declaring method arguments for individual URL / form variables (you don't really want ColdFusion to throw an exception if a user mistypes something in an input field!).
You can also create custom invokers if you need a different behavior such as this invoker to put the result directly into the event object, setting the event argument specified by the resultKey= attribute:
<cfcomponent extends="MachII.framework.ListenerInvoker" output="false" hint="I am a custom invoker" displayName="EventInvoker"> <cffunction name="invokeListener" access="public" returntype="void" output="false" hint="I implement the method invocation for a listener"> <cfargument name="event" type="MachII.framework.Event" required="true" hint="I am the current event" /> <cfargument name="listener" type="MachII.framework.Listener" required="true" hint="I am the listener that is being notified" /> <cfargument name="method" type="string" required="true" hint="I am the method that is being invoked" /> <cfargument name="resultKey" type="string" default="" hint="I name the optional event argument in which the result is stored" /> <cfset var resultVar = 0 /> <cftry> <cfinvoke component="#arguments.listener#" method="#arguments.method#" event="#arguments.event#" returnVariable="resultVar" /> <cfif arguments.resultKey is not ""> <cfset arguments.event.setArg(arguments.resultKey,resultVar) /> </cfif> <cfcatch type="Any"> <cfrethrow /> </cfcatch> </cftry> </cffunction> </cfcomponent>
Since Mach II loads all the framework component instances into application scope,
you need to bear in mind that any instance data you create in your listener
will effectively be stored in application scope. That has three main implications:
variables scope. <cflock type="exclusive" name="..."> .. </cflock> around
any updates on variables scope data, with an appropriately
chosen lock name.In general, your listeners should be stateless - with no instance data - unless
you are specifically caching data for performance reasons, e.g., saving property
values in variables scope to save accessing the properties dynamically
in each request. Consequently, you should take extra care to use var to
declare all local variables in listener methods so that you don't accidentally store
something in the unnamed scope! Remember that tags like <cfquery> create variables too so you must var-declare those as well:
<cfset var userSelect = 0 /> <cfquery name="userSelect" ..> ... </cfquery>
As mentioned above, if you need to manage per-session data in your application,
you will need to use the Session Façade design pattern. The way this
works is that only your listener is session-aware, i.e., it knows about session scope
and manages component instances that live in session scope but
those per-session component instances are not listeners themselves (they do
not extend MachII.framework.Listener)
and they do not reference session scope. For example, a shopping
cart listener would respond to events like 'addItem' and 'removeItem' and 'updateQuantity'
but it would delegate the actions to a cart object that is stored in session scope.
The shopping cart listener would create the cart object in session scope
on demand. The cart object would store information as instance data (which
is per-session because the cart is per-session) but would not reference session scope.
<cfcomponent extends="MachII.framework.Listener" ..>
<cffunction name="addItem" ..>
<cfargument name="item" ../>
<cfset getCart().addItem(arguments.item) />
</cffunction>
...
<cffunction name="getCart" returntype="Cart" access="private" ..>
<cfif not structKeyExists(session,"cart")>
<cfset session.cart = createObject("component","Cart").init() />
</cfif>
<cfreturn session.cart />
</cffunction>
</cfcomponent>
The partial example above demonstrates the Session Façade technique but is not intended to be production quality (for example, there's no configure() method for the listener, there's no hint= or output= attributes, it doesn't lock session scope when creating the cart object which might be needed if you are concerned about threaded access for a single user - which may or may not be a concern for your application).
The basic event lifecycle in Mach II is:
resultKey)If you need more than one piece of data in your view, you could notify the
listener multiple times, creating several resultKeys and having
the view depend on several request scope variables. It seems like
the obvious approach so what is the impact of doing this?
request scope variables are required) These should raise red flags - especially the last two, which cause encapsulation
to break down and coupling to increase. When are you likely to see this pattern
occurring in your application? A good example is a view that displays information
about a person: the view probably needs to display first name, last name, street
address, city, state, zip etc. The obvious - naïve - approach would be to have
getter methods for each of these on the listener (with calls for each of these
in the event handler) and have the view depend on request.firstName, request.lastName,
request.streetAddress etc. Ugly.
The Transfer Object design pattern is intended for situations like this and
solves the problem by aggregating data that is passed between the model and
the view. In the above example, you would have a single getPerson() method
which returns a struct (that contains all of the data needed) and the view
would then have a single dependency on that one resultKey. If
you want a little more encapsulation on your transfer object, you could use
a bean - a simple CFC that has getters and setters for the data and a constructor
(init())
- which the listener component uses to set all the data in the transfer object
before returning it to Mach II.
As indicated above, a bean is a simple CFC with getters and setters that encapsulates
some data (properties). Beans are typically used as Transfer Objects to pass
data between different layers in an application. If a bean has a property foo (a private instance variable) then it also has methods getFoo() and setFoo() to get and set, respectively, the value of foo. The getFoo() method will be
public, the setFoo() method may be public or private depending on whether the
bean is considered read-only or read-write. Here is a simple read-only bean:
<cfcomponent> <!--- "declare" properties for clarity: ---> <cfset variables.foo = "" /> <!--- constructor: ---> <cffunction name="init" returntype="FooBean" access="public" output="false"> <cfargument name="foo" type="string" default="" /> <cfset setFoo(arguments.foo) /> <cfreturn this /> </cffunction> <!--- public getters: ---> <cffunction name="getFoo" returntype="string" access="public" output="false"> <cfreturn variables.foo /> </cffunction> <!--- private setters: ---> <cffunction name="setFoo" returntype="void" access="private" output="false"> <cfargument name="foo" type="string" required="yes" /> <cfset variables.foo = arguments.foo /> </cffunction> </cfcomponent>
A read-write bean differs only in that the setters are public.
The constructor has an optional argument for each property and calls setXxx() for
each property xxx.
Mach II supports bean creation and population through the <event-bean> command:
<event-bean name="beanName" type="beanType" fields="field1,field2" />
This command creates a bean of the specified type (beanType,
e.g., my.model.FooBean)
and stores it in the current event object as an event argument called beanName (e.g., fooBean or
just foo).
fields= is specified, the command calls
the constructor - init() - with no arguments and then calls
the setter for each field specified in the list
of fields (e.g., setField1(event.getArg("field1")), setField2(event.getArg("field2"))). fields= is omitted, the command calls the constructor with
all the current event's arguments by name (e.g., init(field1=event.getArg("field1"),
field2=event.getArg("field2"))).This makes it very easy to handle form submissions in Mach II: define a
bean component to represent the data in a form and use the <event-bean> command
to assemble it from the form data submitted. Then you can operate on
the submitted data as an encapsulated bean, performing validation (see Designing
Event Filters) and persistence and whatever else you need.
For more information on writing and using beans, refer to the Mach II article Beans, Beans, the Musical Fruit by Ben Edwards and Hal Helms.
Whilst this topic is not directly related to the Mach II framework, most applications need to implement data access (usually to a database) and many people ask for guidance on this subject.
There are two basic patterns of access to persistent data within most applications:
ColdFusion has a great built-in idiom for dealing with the first type of access
- the query - which is an efficient way to manipulate (potentially
large) sets of data rows retrieved from a database (or other data sources,
since you can easily create a query object and populate it with
your own data). When you are dealing with aggregated access, it does not make
sense to convert every row returned into a fully-encapsulated object (CFC instance)
when all you are likely to do with the data is display a few fields with a
link to a detail page that will focus on the selected row.
On the other hand, when you are focusing on a single row it usually does make sense to work at the fully-encapsulated object level since you are usually interested in object behavior at that point. This is also the level where you need the standard CRUD (Create, Read, Update, Delete) operations.
Recognizing these two basic patterns, you should design your components accordingly by providing separate components for each pattern. This is best explained through an example:
Order, then we would
provide an OrderGateway component for aggregate access and an OrderDAO component
for per-object access (or build the per-object access into the Order object
- but see below).OrderGateway component would provide methods like findAll(),
findWhere(), findByID() and they would all return standard query objects
(even findByID() which returns a single row).OrderDAO component would provide CRUD methods like store(), load(),
update(), delete() and they would operate on a
specific Order object exchanging
data via getters/setters on the Order component or via some
sort of snapshot of the Order's data, e.g., a bean: the Order component
could implement methods like getSnapshot() returning a bean and setSnapshot() taking a bean as an argument - the bean containing the core persistent data for the Order object.
Sometimes a more direct data transfer between the business object and the
data access object is needed for performance reasons (or because the application
can 'trust' the components to exchange less encapsulated data, such as a
struct or some opaque data structure e.g., the Memento design pattern).
Such optimizations are beyond the scope of this document.Separating out these operations from the business model object helps it stay persistence-neutral. The gateway components can be optimized for retrieving large record sets, caching etc. The DAO components can be optimized for dirty data updates, pooled object access and so on. Again, the details of these optimizations are beyond the scope of this document but providing for the two distinct patterns of data access will get you started on the right road.
As noted above, you can implement the CRUD methods directly in your business model object if you wish, although these guidelines recommend using the separate DAO component outlined above. The pros of implementing CRUD in your business model object are:
The cons are:
As you can imagine, it is the driving forces of good encapsulation, high cohesion and loose coupling that lead to the recommendation above to provide gateway and DAO components that are separate from the business model components.
To see more concrete examples of these concepts, visit mach-ii.info and download the sample application which uses a number of these design patterns.
« Application Structure & Design | Contents | Designing Views »
RSS feed | Send me an e-mail when comments are added to this page | Comment Report
Current page: http://livedocs.adobe.com/wtg/public/machiidevguide/models.html
Comments
cducker said on Dec 8, 2003 at 6:32 PM :