Applying Design Patterns in ColdFusion - The Observer Pattern
Coldfusion, Design PatternsI have to say, I'm pretty excited about how the observer pattern works. Coming from the mindset of event driven programming and asynchronous communication, it seems to have many similar traits. Head First Design Patterns describes the observer patterns as:
The Observer Pattern defines a one-to-many dependancy between objects so that when one object changes state, all of its dependants are notified and updated automatically
Or to put it another way, the Observer Pattern allows one or many objects, in this case we call them observers, to register themselves with a single object, the subject, in order to have the data they need pushed to them from the subject. How each of the observers reacts to the data can vary greatly.
In the real world you might see something like this. Journalists will register to be present at Apple's WWDC keynote. Steve Jobs will come out on stage and deliver mind blowing information about the new iHammock, the worlds first hammock and media player all-in-one. Each journalist has a different reaction and will publish articles reflecting different points. In this scenario the journalists would be the observers and Steve Jobs would be the subject. The observers don't have to ask for information, the subject just delivers it without knowing the intention of the observer.
We need to ask ourselves a couple of questions before we start modeling or coding our components. What methods do we need in our subject and observers and how can we ensure consistency in our objects and guarantee the methods we define will be present in our objects?
Let's talk about methods.
The Subject:
We need a way for objects to register themselves as observers, a way for objects to remove themselves from the observer pool, and a way to notify the observers with new data. These are really the three core behaviors and any other behaviors are just gravy. Here are the methods I'll be using in my QuoteData subject.
registerObserver( ) : Will accept an observer object as an argument, adds observer to a private array and return void.
removeObserver( ) : Accepts an observer object as an argument and removes the matching object from the array.
notifyObservers( ) : Loops over the array of observers and calls an update( ) method on each of them.
And the last two that are not really part of the pattern, but are needed to make the subject breath.
init ( ) : The pseudo constructor. Creates a new array to hold the observers then returns itself.
setQuote( ) : Takes a slew of stock quote data as arguments and stores it in a struct in the variables scope. It then calls the notifyObservers( ) method.
The observers actually have only one required method, well two if you count init( ).
update( ) : Takes quote data as arguments and stores it in a struct in the variables scope. It then reacts to the data and does stuff.
init( ) : Pseudo constructor. Accepts the subject as an argument then registers itself with the subject.
getID( ) : Getting that returns the ID (UUID) of the object. This is needed because ColdFusion can't evaluate and compare two complex objects.
You might be wondering why I didn't just show the code for the methods. I think it's premature to code these objects without first talking about interfaces. Not the UI, but the interface an object can implement to define what methods are required in an object. This leads me into the next question about adding consistency and being able to gaurantee that methods will be defined. In Java and ActionScript, and many other languages, the concept of interfaces is nothing new, however in ColdFusion we didn't have interfaces until version 8. Because of this and because I'll be using interfaces throughout my tutorials and demos I highly recommend you take a look at Dave Shuck's post on cfinterface for the why's and how's as the topic is a bit out of the scope of this post.
I'll be using three interfaces in my application, see them below.
com.Interface.Subject
<cfinterface> <cffunction name="registerObserver" returntype="void" output="false"> <cfargument name="observer" required="true" /> </cffunction> <cffunction name="removeObserver" returntype="void" output="false"> <cfargument name="observer" required="true" /> </cffunction> <cffunction name="notifyObservers" returntype="void" output="false"> </cffunction> </cfinterface>
com.Interface.Observer
<cfinterface> <cffunction name="update" returntype="void" output="false"> <cfargument name="symbol" required="true"/> <cfargument name="last" required="true"/> <cfargument name="date" required="true"/> <cfargument name="time" required="true"/> <cfargument name="change" required="true"/> <cfargument name="earns" required="true"/> <cfargument name="open" required="true"/> <cfargument name="high" required="true"/> <cfargument name="low" required="true"/> <cfargument name="volume" required="true"/> <cfargument name="pe" required="true"/> <cfargument name="mktCap" required="true"/> <cfargument name="previousClose" required="true"/> <cfargument name="percentageChange" required="true"/> <cfargument name="annRange" required="true"/> <cfargument name="name" required="true"/> </cffunction> <cffunction name="getID" returntype="string" output="false"> </cffunction> </cfinterface>
com.Interface.DisplayElement
<cfinterface> <cffunction name="display" returntype="struct" output="false"> </cffunction> </cfinterface>
Let's dissect the code used in the QuoteData subject. This object implements the com.Interface.Subject interface which enforces that we use the methods defined (see above).
The init method doesn't do much. It creates a new array that will be used to store observers, then it returns itself.
<cffunction name="init" access="public" returntype="QuoteData" output="false"> <cfset variables.observers = ArrayNew(1) /> <cfreturn this /> </cffunction>
The regsterObserver method accepts the observer as an argument and appends it to the observers array.
<cffunction name="registerObserver" access="public" returntype="void" output="false"> <cfargument name="observer" required="true" /> <cfset ArrayAppend(variables.observers,arguments.observer) /> <cfreturn /> </cffunction>
The removeObserver method takes an observer as an argument, finds it in the observer array and removes it. Nothing tricky here.
<cffunction name="removeObserver" access="public" returntype="void" output="false">
<cfargument name="observer" required="true" />
<cfset var local = {} />
<cfif ArrayLen(variables.observers) GT 0>
<cfloop from="1" to="#ArrayLen(variables.observers)#" index="local.i">
<cfif variables.observers[local.i].getID() IS arguments.observer.getID()>
<cfset ArrayDeleteAt(variables.observers, local.i) />
<cfbreak />
</cfif>
</cfloop>
</cfif>
<cfreturn />
</cffunction>The notifyObservers method loops over the observers array and calls the update() method on each observer.
<cffunction name="notifyObservers" access="public" returntype="void" output="false"> <cfif ArrayLen(variables.observers) GT 0> <cfloop from="1" to="#ArrayLen(variables.observers)#" index="local.i"> <cfset variables.observers[local.i].update(ArgumentCollection = variables.quote) /> </cfloop> </cfif> <cfreturn /> </cffunction>
Lastly, the setQuote method. This one takes stock quote data as the arguments, there's more data than I thought! Then, to save myself some typing, I just duplicate the arguments scope into the variables.quote struct and call the notifyObservers() method.
<cffunction name="setQuote" access="public" returntype="void" output="false"> <cfargument name="symbol" required="true"/> <cfargument name="last" required="true"/> <cfargument name="date" required="true"/> <cfargument name="time" required="true"/> <cfargument name="change" required="true"/> <cfargument name="earns" required="true"/> <cfargument name="open" required="true"/> <cfargument name="high" required="true"/> <cfargument name="low" required="true"/> <cfargument name="volume" required="true"/> <cfargument name="pe" required="true"/> <cfargument name="mktCap" required="true"/> <cfargument name="previousClose" required="true"/> <cfargument name="percentageChange" required="true"/> <cfargument name="annRange" required="true"/> <cfargument name="name" required="true"/> <cfset variables.quote = Duplicate(arguments) /> <cfset notifyObservers() /> <cfreturn /> </cffunction>
Next take a look at the SimpleQuoteDisplay observer object. It implements the com.Interface.Observer and the com.Interface.DisplayElement interfaces.
The init method takes a subject as an argument, sets the id property, stores and registers itself with the subject, then it returns itself.
<cffunction name="init" access="public" returntype="SimpleQuoteDisplay" output="false"> <cfargument name="quoteData" required="true"/> <cfset variables.id = CreateUUID() /> <cfset variables.quoteData = arguments.quoteData /> <cfset variables.quoteData.registerObserver(this) /> <cfreturn this /> </cffunction>
The update method, which is enforced by the Observer interface, takes a bunch of quote data arguments but only stores what it needs.
<cffunction name="update" access="public" returntype="void" output="false"> <cfargument name="symbol" required="true"/> <cfargument name="last" required="true"/> <cfargument name="date" required="true"/> <cfargument name="time" required="true"/> <cfargument name="change" required="true"/> <cfargument name="earns" required="true"/> <cfargument name="open" required="true"/> <cfargument name="high" required="true"/> <cfargument name="low" required="true"/> <cfargument name="volume" required="true"/> <cfargument name="pe" required="true"/> <cfargument name="mktCap" required="true"/> <cfargument name="previousClose" required="true"/> <cfargument name="percentageChange" required="true"/> <cfargument name="annRange" required="true"/> <cfargument name="name" required="true"/> <cfset variables.quote.symbol = arguments.symbol /> <cfset variables.quote.last = arguments.last /> <cfset variables.quote.date = arguments.date /> <cfset variables.quote.time = arguments.time /> <cfset variables.quote.change = arguments.change /> <cfset variables.quote.name = arguments.name /> <cfreturn /> </cffunction>
The display method returns a struct of the stored quote data. This method is enforced by the DisplayElement interface.
<cffunction name="display" access="public" returntype="struct" output="false"> <cfreturn variables.quote /> </cffunction>
The getID method just returns the id of the object. This is used to remove the observer from the subject's observers array.
<cffunction name="getID" access="public" returntype="string" output="false"> <cfreturn variables.id /> </cffunction>
All other observer objects will follow the same pattern adding in any business logic they may need. By doing this we've made adding in new functionality a snap, so-to-speak. Let's say that you wanted an observer that instead of spitting out the raw stock data it would compare trending data from some persistence mechanism then create and store tabular values used for charting. The new observer would be written and implemented without having to change the subject or any of the other observers.
The last thing we need to do before outputting data is instantiate these objects and get the observers to register with the subject. In my demo app I'm doing this in the application scope since nothing is contained in the objects that would be session specific. This also allows me to have a scheduled routine that retrieves stock quote data from a web service and feed it to the subject. This means that when I make requests to my observer's display() method I already have the latest data and don't need to wait for the web service call to complete. It also means that I make a single call to the web service rather than 1 call per observer request. Here is a snippet of how I am instantiating the subject and observers in my Application.cfc.
<cffunction name="cacheObjects" access="private">
<cfif StructKeyExists(application, 'obj')>
<cfset StructClear(application.obj) />
</cfif>
<cfset application.obj = {} />
<!--- cache objects in the application scope --->
<cfset application.obj.quoteData = CreateObject('component','com.ValueObject.QuoteData').init() />
<cfset application.obj.simpleQuoteDisplay = reateObject('component','com.ValueObject.SimpleQuoteDisplay').init(application.obj.quoteData) />
<cfset application.obj.extraQuoteDisplay = reateObject('component','com.ValueObject.ExtraQuoteDisplay').init(application.obj.quoteData) />
<!--- service objects --->
<cfset application.obj.service = {} />
<cfset application.obj.service.quoteService = CreateObject('component','com.Service.QuoteService').init() />
</cffunction>To make everything work, I built a simple UI in html and jQuery that makes a json call to a facade that calls the display() method of the observer and returns the serialized struct. Here's one of the facade methods demonstrating this.
<cffunction name="getQuote" access="remote" returntype="struct" output="false" returnFormat="json"> <cfreturn application.obj.simpleQuoteDisplay.display() /> </cffunction>
I think this sums up pretty well how to implement the Observer Pattern in your CF applications. The immediate flexibility offered here is pretty awesome. If you have any cases where you've implemented this pattern in an application I'd love to hear about it!
Check out the full demo here: http://portfolio.lanctr.com/StockQuoteDemo/
Download the zipped project here: http://slantsoft.com/files/ServeFile.cfm?file=ObserverPatternStockQuoteDemo.zip






Loading....