View comments | RSS feed

Mach II Development Guide - Designing Models

Release 1.2.1 (4/14/2005)

« Application Structure & Design | Contents | Designing Views »

Printable Version

Designing Models For Mach II

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.

Separation of the Business Model and Mach II

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.

Anatomy Of A Listener

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.

Invokers & Listeners

When a listener is declared in mach-ii.xml, you have to specify an invoker type. Mach II provides two default types:

The 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:

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>

Instance Data & Listeners

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:

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>

Session Façade

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).

Transfer Objects

The basic event lifecycle in Mach II is:

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?

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.

Beans & Form Handling

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).

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.

Database Access Objects

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:

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 »

Comments


cducker said on Dec 8, 2003 at 6:32 PM :
Have you guys encountered any proformance problems in big applications where the mach-ii.xml becomes really large due to the number of event handlers, listeners, views, etc?
SeanCorfield said on Dec 19, 2003 at 10:40 AM :
We have not encountered any performance problems with large XML files.
We have high traffic applications on macromedia.com that have mach-ii.xml
files containing 1,000 lines or more.
Since the XML file is read only at startup, the size of it should not affect an
application when it is running.
cducker said on Dec 22, 2003 at 11:54 AM :
I see, so as long as you have enough memory it should be fine.

Doesn't maintaining the mach-ii.xml file become a pain when it's over 1,000 lines? Have you guys thought of a method to package related event handlers, etc into smaller mach-ii.xml files so they're more managable? The main mach-ii.xml file could contain paths to each of the smaller .xml files and when the app is loaded it could merge all the .xml docs into one mach-ii xml object and then load that into memory?
SeanCorfield said on Dec 22, 2003 at 7:46 PM :
Actually, we're not finding the single XML file to be a problem - we
have lots of CFCs that are bigger than that already.
No screen name said on Apr 12, 2004 at 8:18 PM :
That justification is somewhat flawed. Very large CFC's are fine so long as they are cohesive. It is very likley that a single very large mach-ii.xml file will not be cohesive. There is no good reason not to have a cohesive xml file.

Some sort of subsidiary xml file directive would be very useful in maintaining a cohesive xml file and also would allow more than one developer to simultaneously maintain those cohesive 'islands' of configuration xml.
SeanCorfield said on Apr 13, 2004 at 10:56 AM :
The comparison between the size of a CFC file and the size of the XML file was just meant to indicate that size per se has not proved to be a problem.

According to threads on the Mach II forum, folks who've tried to break XML files into pieces and use some sort of preprocessing directive, as you describe, have found that the effort is not worthwhile and managing multiple files is actually more trouble than dealing with a single large file.
Source code control - with a built-in merge - really helps multi-developer teams work together. We use CVS and find it invaluable.
No screen name said on Apr 18, 2004 at 12:27 AM :
Ahh, so you DO have a segmented configutation document, but use external processes to manage it, as opposed to using mach-ii processes. Fair enough. Fair enough.
SeanCorfield said on Apr 18, 2004 at 12:37 PM :
No, we don't have a segmented configuration file. CVS merge is used to reconcile edits by multiple developers to the same file, not to take multiple files and join them together.
No screen name said on May 24, 2004 at 8:14 PM :
If I remember it correctly, mach-ii.xml can be replace by mach-ii.xml.cfm
In this way, I think you can group your event-handlers as you like by simply using <cfinclude> like this :
<event-handlers>
<cfinclude template="XeventHandlers.xml.cfm" />
<cfinclude template="YeventHandlers.xml.cfm" />
<cfinclude template="ZeventHandlers.xml.cfm" />
</event-handlers>

By doing this way, I can see the event-handlers are properly grouped and no modification in mach-ii core file is needed.


crab
SeanCorfield said on May 25, 2004 at 8:10 AM :
Since mach-ii.xml is read using the cffile tag, it would not be processed by ColdFusion even if it had a .cfm extension (so your suggestion will not work, I'm afraid).
No screen name said on Jun 3, 2004 at 5:47 PM :
I'm experiencing some serious performance problems using Mach II. I'm doing very minor I/O (XML) on each request that would normally take a few hundred milliseconds. Using Mach II, the average page load takes 3000 ms or more.

Perhaps I'm making MVC newbie mistakes, but they aren't quite obvious. My code is hardly any different from the samples provided on mach-ii.com. I'd be willing to share the code if anyone wants to look at it.
SeanCorfield said on Jun 4, 2004 at 10:11 AM :
Make sure you have MACHII_CONFIG_MODE set to 0 or -1 so that
Mach II does not reload the XML configuration file on each request.
You should be seeing basic request times around 30-50ms.
sanaullah said on Jul 7, 2004 at 5:16 AM :
Using cflock is problem in Jrun clustering .... when we are using session replicationin clustering environment. The code works fine without cflock ..... even i have applied session Façade... have probelem with unique cflock
sanaullah said on Jul 7, 2004 at 5:26 AM :
one more thing i really like that is memento way of getting and setting of data... i have continous steps forms , in that way a simple memento variables can holds many form fields and very easy to manipulate... in some situaiton we can use bean, but honestly your first method was excellent. I would certainly like to see some more indepth detail about memento way of handling in machII.
SeanCorfield said on Jul 7, 2004 at 9:13 AM :
Bear in mind that in CFMX 6.1, CFCs in session scope will not replicate across a cluster. Also, cflock does not operate across a cluster (locking is per server instance).
cedgar said on Oct 1, 2004 at 3:24 AM :
I am keen to implement Mach II especially since we are doubling our developers in the near future. However the topic regarding CFC's not replicating in a clustered environment is causing me to rethink this direction change.

Is there a justified excuse for macromedia leaving out this feature in the current MX 6.1 release?

Do Macromedia intend on enabling this feature in the future, if so when?

Is there any workarounds, I know STICKY SESSIONS is available but that doesn't help when one or more servers go down?

Chris
SeanCorfield said on Oct 1, 2004 at 7:40 AM :
macromedia.com uses a hardware load balancer rather than JRun's built-in load balancer and uses sticky session (on the load balancer).

You can read about the macromedia.com architecture in a Developer Center article by Brandon Purcell and Frank DeRienzo.

We have not found the lack of CFC replication to be a problem (and we use CFCs in session scope very heavily). The shopping cart in the online store is a session CFC but because we have a persistent cart - stored to the database - the session CFC would be recovered if a server went down during a customer transaction.
cedgar said on Nov 12, 2004 at 7:09 AM :
Is there a best practice for handling non-required form data that map onto ColdFusion boolean, date and numeric datatypes?

I cannot see an elegant solution when trying to use the <event-bean> tag with a strongly typed CFC bean.

Chris
SeanCorfield said on Nov 12, 2004 at 7:31 AM :
For a mixture of required and non-required fields, you can omit the field list from the event-bean tag and let it do the initialization of your bean through the init() method.

Your init() method would have cfargument tags for all the form fields and specify required="true" or required="false" as appropriate.
SRI-CTPino said on Apr 13, 2006 at 4:53 PM :
Can a series of cfargument tags within the init method declaration be enclosed with a cftry block or blocks?
SeanCorfield said on Apr 14, 2006 at 3:31 PM :
No, you cannot put a try block around the argument tags. If you want more control over argument validation, you need to define arguments as string type (or any type) and then perform explicit validation on the arguments yourself.

However, your model probably should be strongly typed, except for beans that are used to handle user input. The controller layer should ensure that all the necessary validation is performed on user input (via the beans).

Any invalid arguments passed to your core model should then be considered unexpected errors - exceptions - and it is appropriate for cfargument's type specification to handle that (and throw an exception that the controller can catch and decide how to notify the user about the programming logic failure that has occurred).

 

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