| Close Window |
While standing in line at my local coffee shop the other day, I was thinking about how helpful a thorough knowledge of design patterns is for a developer. In case you're unclear about what design patterns are, think of them as time-tested solutions to very specific problems.
I was thinking of design patterns as similar to woodworking joints. Each joint is a time-tested solution to a very specific problem. For example, what joint would a woodworker use where the joint will be subjected to being pulled apart - something that occurs with drawers? The best design pattern for this situation is a dovetail joint - one that gets tighter as tension is applied. If you don't know about the dovetail joint or don't know how to cut one, your drawer is likely to fail over time.
Programming design patterns offer similar help to programmers. Because most books on design patterns don't have ColdFusion examples, learning the repertoire of design patterns is especially difficult for us ColdFusion programmers.
All of this was rumbling through my head when I realized I had been in line for over 10 minutes. Now, I'm as understanding as the next guy. I understand that the patrons ahead of me are eager to get their beverage just right. But after 13 minutes of waiting for such esoteric orders as "Half-caf extra hot low-foam cappuccino with an expresso shot and hazelnut syrup," well, a man can stand only so much.
That's how I got the idea for my new venture: a fully automated coffee shop! Of course, the name is important and so I sought professional advice from a branding firm. I spoke with their VP for Really Cool Names, J. Billingsly Farnsworth III, and explained my idea.
"I like it!" exclaimed J. "It's bold! It's innovative! And I have just the name for it!" (Marketing people, I found out, speak almost exclusively in exclamation marks.)
"What did you come up with?" I asked.
"Ready? Here it is: StarBoard Coffee!"
"StarBoard?" I asked. "Isn't that a little close to...you know?"
"Ha! We'll leverage their brand equity to achieve a market penetration that will place us in a position to enter the constant-growth economy!"
"Umm...I see," I lied.
"And I have your tagline! You ready for this? Here it is: ?We Do Coffee Right'"
"Err...isn't that awfully close to another company's tagline?" I asked nervously.
"Yeah, but they mean ?right' as in ?correct' and we mean ?right' as in the direction! Get it? Starboard: Right. It's brilliant! I've run it by our permanent in-house focus group using a galvanometer test and they concur! Now, I've put together an advertising plan that'll we'll narrowcast based on lifestyle segmentation and psychographics..."
He said a lot more, but I gathered that with his marketing and my idea, I was well on my way to joining the pantheon of superstar CEOs. Only one thing remained: actually creating the AutoBarista.
The real secret to success would be creating a machine that would provide the kind of flexibility that today's coffee consumers demand while making future changes and upgrades simple. The hardware part was easy enough, but getting the software right would be a challenge.
I started with the prototype. This would be the menu customers would encounter at any one of the thousands of stores I had planned (see Figure 1).
Here is the code that produces the menu:
<h2>Please select your StarBoard beverage!</h2> <form action="Order.cfm" method="post"> <h3>Beverage type</h3> <p> <input type="Radio" name="beverage" value="coffee" checked /> Coffee <br /> <input type="Radio" name="beverage" value="tea" /> Tea </p> <h3>Standard Options</h3> <p> <input type="Checkbox" name="options" value="Cream" /> Cream <br /> <input type="Checkbox" name="options" value="SoyMilk" /> Soy milk<br /> <input type="Checkbox" name="options" value="ExpressoShot" /> Expresso shot<br /> </p> <h3>Flavored Syrups</h3> <p><input type="Checkbox" name="syrups" value="Almond" /> Almond<br /> <input type="Checkbox" name="syrups" value="Vanilla" /> Vanilla<br /> <input type="Checkbox" name="syrups" value="Hazelnut" /> Hazelnut<br /> <input type="Checkbox" name="syrups" value="NewCar" /> New car <br /></p> <br /> <input type="Submit" value="Place order" /> </form>
Having worked on many projects, I knew that the most important thing I could do to help ensure my new creation's success was to provide it with ease of maintenance. After all, 70-90% of an application's cost throughout its entire life cycle is spent on maintenance.
Now, when I think of ease of maintenance, I think of object orientation. This is one of OO's great strengths, after all. So, I began thinking about the various CFCs I would need for my AutoBarista. Each class would need methods for returning the cost and description of each coffee drink. I began listing some of the classes I would need:
"Better ideas" are exactly what design patterns provide. They deal with recurring problems that often have no obvious, or simple solution. Take my problem, for example: class explosion. One particular design pattern, the Decorator pattern, provides time-tested, expert guidance on how to deal with the problem of base objects with multiple options while avoiding the problem of class explosion.
To understand how decorators work, let's first look at how I might model coffee and tea (without options) using CFCs. Since there is a great deal of similarity between coffee and tea, I've chosen to abstract common functionality into a superclass, Beverage (see Figure 2).
If you're new to the UML class diagrams I'm using, here's a quick primer. Each box contains three sections. The top section is the name of the class. The middle section (empty for Coffee and Tea) describes any properties (data) associated with the class. The bottom section specifies the methods associated with the class. The arrows point from the subclass to the superclass.
Here is the code for these three classes.
<cfcomponent displayname="Beverage">
<cfset variables.cost = 0 />
<cfset variables.description = "" />
<cffunction name="getCost" access="public" returntype="numeric" output="false">
<cfreturn variables.cost />
</cffunction>
<cffunction name="setCost" access="public" returntype="void" output="false">
<cfargument name="cost" type="numeric" required="true" />
<cfset variables.cost = arguments.cost />
</cffunction>
<cffunction name="getDescription" access="private" returntype="string" output="false">
<cfreturn variables.description />
</cffunction>
<cffunction name="setDescription" access="private" returntype="void" output="false">
<cfargument name="description" type="string" required="true" />
<cfset variables.description = arguments.description />
</cffunction>
<cffunction name="asString" access="public" returntype="string" output="false">
<cfreturn getDescription() />
</cffunction>
</cfcomponent>
Code for Beverage.cfc
<cfcomponent displayname="Coffee" extends="Beverage">
<cffunction name="init" access="public" returntype="Coffee" output="false">
<cfset setCost(getCost() + 2.55) />
<cfset setDescription("Coffee " & getDescription()) />
<cfreturn this />
</cffunction>
</cfcomponent>
Code for Coffee.cfc
<cfcomponent displayname="Tea" extends="Beverage">
<cffunction name="init" access="public" returntype="Tea" output="false">
<cfset setCost(getCost() + 2.70) />
<cfset setDescription("Tea " & getDescription()) />
<cfreturn this />
</cffunction>
</cfcomponent>
Code for Tea.cfc
Most of the functionality is located in the Beverage class. Coffee and Tea have a pseudo-constructor, init, that initializes the objects created from these classes.
The problem of adding what could work into many options for each different beverage is a daunting one. The first "solution" I arrived at (class explosion) is no solution at all. But the Decorator pattern offers a solution that is flexible and manageable.
A Decorator is a clever thing. It nests or wraps the base object within itself. The Decorator is the same data type as the object it decorates. In my case, all Decorators will extend the Beverage class, making each decorator a Beverage. The Decorator can then be used for wherever the base object is called. And because the Decorator has access to the base object, it can call any nonprivate methods on it that it needs to.
To see how this works, let's consider the case of a customer who wants to add a shot of vanilla syrup to his or her coffee (see Figure 3).
In ObjectLand, I would create the base coffee object with this code:
<cfset beverage = CreateObject('component', 'Coffee').init() />
Then, I can decorate the beverage with a SyrupDecorator:
<cfset beverage = CreateObject('component', 'SyrupDecorator').init(beverage, ?vanilla') />
Since both the original beverage and the decorated beverage are of type Beverage, any method that's expecting a Beverage object will accept either a base beverage or a decorated beverage: both have the same data type and both will respond to the "getCost" and "asString" methods.
We'll see shortly one particularly important method that will accept a beverage. First, here is the code for the superclass, Decorator.cfc, and one of the Decorator subclasses, SyrupDecorator.cfc:
<cfcomponent displayname="Decorator" extends="Beverage"> <cfset variables.baseObject = "" /> <cffunction name="getBaseObject" access="private" returntype="Beverage" output="false"> <cfreturn variables.baseObject /> </cffunction> <cffunction name="setBaseObject" access="private" returntype="void" output="false"> <cfargument name="baseObject" type="Beverage" required="true" /> <cfset variables.baseObject = arguments.baseObject /> </cffunction> </cfcomponent> Code for Decorator.cfc <cfcomponent displayname="SyrupDecorator" extends="Decorator"> <cffunction name="init" access="public" returntype="SyrupDecorator" output="false"> <cfargument name="baseObject" type="Beverage" required="true" /> <cfargument name="flavor" type="string" required="true" /> <cfset setBaseObject(arguments.baseObject) /> <cfset setDescription( getBaseObject().asString() & " with " & arguments.flavor & " syrup") /> <cfset setCost(getBaseObject().getCost() + .55) /> <cfreturn this /> </cffunction> </cfcomponent>
The init method accepts two arguments, a baseObject (of type Beverage) and a string describing the flavor of the syrup to be added to the customer's coffee order. This init method calls the "setBaseObject" method of its parent, Decorator, thereby allowing the decorator to hold the beverage passed into it as an instance variable.
Fine, you say. But how does this help us? Let's take a look at another class that I certainly hope will be getting a lot of use, the CashRegister.
<cfcomponent displayname="CashRegister"> <cffunction name="init" access="public" returntype="CashRegister" output="false"> <cfreturn this /> </cffunction> <cffunction name="order" access="public" returntype="void" output="true"> <cfargument name="beverage" type="Beverage" required="true" /> You have selected our fine #arguments.beverage.asString()#. That will be #DollarFormat(arguments.beverage.getCost())# <br /> </cffunction> </cfcomponent>
Notice that there is a single "order" method that describes the beverage ordered and shows the customer the amount owed by calling the "getCost" method of the beverage passed into the CashRegister.
Now, when a customer orders a simple coffee (no options), an object of type Coffee will be created and passed to the CashRegister's "order" method. Since a coffee is a Beverage, it has a "getCost" method and, in the Coffee CFC, the init method specifies that the base price for a cup of coffee is a mere $2.55.
But when a customer wants a cup of coffee with flavored syrup added to it, I create the Coffee object and then pass it in to the SyrupDecorator's init method (where the Coffee object is registered as the SyrupDecorator's base object). Notice that in SyrupDecorator (and in all the Decorator subclasses), the "getCost" method has been changed from the initial version in Beverage. Each Decorator subclass says that its cost is whatever its base object's cost is plus any add-on for the option. In the case of flavored syrups, that add-on is $0.55.
Now that I have the SyrupDecorator object (with its base Beverage object), I can pass this to the CashRegister's "order" method. This is an example of the benefits of loose coupling between components and designing to interfaces rather than to implementations.
All well and good, you say, but what if the customer wants a coffee with vanilla syrup and a shot of expresso? Just decorate the decorator. In other words, since all decorators have an init method that accepts a base object of type Beverage, create a SyrupDecorator that decorates a Coffee object and then decorate the SyrupDecorator with an ExpressoShotDecorator (see Figure 4).
The code for this would look like this:
<cfset beverage = CreateObject('component', 'Coffee').init() />
<cfset beverage = CreateObject('component', 'SyrupDecorator').init(beverage, ?vanilla') />
<cfset beverage = CreateObject('component', 'ExpressoShotDecorator').init(beverage) />
Order.cfm, the page that the user selection form posts to, handles this with a few short lines of code:
<!--- If needed, create cash register --->
<cfif NOT IsDefined('application.cashRegister')>
<cfset application.cashRegister = CreateObject('component', 'CashRegister').init() />
</cfif>
<!--- Create base beverage --->
<cfset beverage = CreateObject('component', '#form.beverage#').init() />
<cfparam name="form.options" default="" />
<cfparam name="form.syrups" default="" />
<!--- If any non-syrup options were selected, create decorators for these --->
<cfloop list="#form.options#" index="option">
<cfset beverage = CreateObject('component', '#option#Decorator').init(beverage) />
</cfloop>
<!--- If any syrup options were selected, create decorators for these --->
<cfloop list="#form.syrups#" index="syrup">
<cfset beverage = CreateObject('component', 'SyrupDecorator').init(beverage, syrup) />
</cfloop>
<!--- Pass the beverage (decorated or not) to cash register --->
<cfset application.cashRegister.order(beverage) />
Of course, you can keep on decorating decorators until you have built up your base object with all the options required. Here's the UML for the Decorator CFCs I added to my original design (see Figure 5).
Notice the new type of connector between Beverage and Decorator. This line with a diamond (rather than an arrow) indicates that the Decorator class will have a property of type Beverage. This is the baseObject that it decorates. The "init" method of each of the Decorator subclasses specifies that it accepts a baseObject of type Beverage. Once a baseObject is provided in the "init" method, each Decorator will have access to the baseObject throughout its life.
Here's the code for each of the other Decorators:
<cfcomponent displayname="CreamDecorator" extends="Decorator"> <cffunction name="init" access="public" returntype="CreamDecorator" output="false"> <cfargument name="baseObject" type="Beverage" required="true" /> <cfset setBaseObject(arguments.baseObject) /> <cfset setCost(getBaseObject().getCost()) /> <cfset setDescription( getBaseObject().asString() & " with cream ") /> <cfreturn this /> </cffunction> </cfcomponent> Code for CreamDecorator.cfc <cfcomponent displayname="ExpressoShotDecorator" extends="Decorator"> <cffunction name="init" access="public" returntype="ExpressoShotDecorator" output="false"> <cfargument name="baseObject" type="Beverage" required="true" /> <cfset setBaseObject(arguments.baseObject) /> <cfset setCost(getBaseObject().getCost() + .55) /> <cfset setDescription(getBaseObject().asString() & " with shot of expresso ") /> <cfreturn this /> </cffunction> </cfcomponent> Code for ExpressoShotDecorator.cfc <cfcomponent displayname="SoyMilkDecorator" extends="Decorator"> <cffunction name="init" access="public" returntype="SoyMilkDecorator" output="false"> <cfargument name="baseObject" type="Beverage" required="true" /> <cfset setBaseObject(arguments.baseObject) /> <cfset setDescription( getBaseObject().asString() & " with soy milk ") /> <cfset setCost(getBaseObject().getCost() + .35) /> <cfreturn this /> </cffunction> </cfcomponent> Code for SoyMilkDecorator.cfc
Unless you've seen the Decorator pattern previously, the UML diagram probably didn't help you too much, so let's break it down further. Note that the Decorator class extends Beverage. This means that by the magic of polymorphism, a Decorator is a Beverage and can be substituted for a Beverage whenever a Beverage is called for.
When might a Beverage object be called for? What about a CashRegister object with an "order" method as shown in the code for Order.cfm? The purpose of this method will be to describe what the customer has ordered and to provide the price for all this goodness. Here is the code for CashRegister.cfc:
<cfcomponent displayname="CashRegister"> <cffunction name="init" access="public" returntype="CashRegister" output="false"> <cfreturn this /> </cffunction> <cffunction name="order" access="public" returntype="void" output="true"> <cfargument name="beverage" type="Beverage" required="true" /> You have selected our fine #arguments.beverage.asString()#. That will be #DollarFormat(arguments.beverage.getCost())# <br /> </cffunction> </cfcomponent>
The CashRegister asks each Beverage sent into it for the Beverage's description (using the "asString" method) and its cost (using the "getCost" method). Because it accepts a generic "Beverage," there's no need to have the many different methods that would be needed to reflect all the combinations of base beverages and options.
I said that one of the strengths of object-oriented programming is that it eases the burden of maintaining code. Here's what I mean: if we later decide to add an option - perhaps, a shot of Irish Cream - all we need to do is create a new IrishCreamDecorator CFC and add the new option to our menu page. No other code will be affected. Similarly, if I no longer decide to offer an option, I can remove the CFC and remove the display code from Menu.cfm. No other code is affected.
Decorators are a design pattern meant to solve a very specific problem, one in which a base object may have many separate options. Learning how to use the Decorator pattern will give you the ability to use just the right tool in just the right place.
© 2008 SYS-CON Media Inc.