ColdFusion 10 - Selectively Exposing ColdFusion Component Behaviors - Part II
Yesterday, I looked at using ColdFusion 10's Closure syntax as a light-weight, class-free way to expose selective behaviors on ColdFusion components. This was inspired by a Douglas Crockford presentation that talked about giving objets only as much authority as they need to get the job done - and no more. To continue my exploration of this new topic, I wanted to look at it again, this time with a slightly more real-world use-case in mind.
NOTE: At the time of this writing, ColdFusion 10 was in public beta.
For this demo, imagine that I have an online eCommerce website. Now, also imagine that this website needs to send out a variety of formatted, HTML emails to its customers. In order to simplify the creation, maintenance, and delivery of these emails, the actual rendering of the email content has been factored-out into its own ColdFusion component: EmailService.cfc. For this eCommerce site, this EmailService.cfc component has the following methods:
- sendInvoiceEmail()
- sendNewsletterEmail()
- sendPasswordResetEmail()
- sendWelcomeEmail()
As you can see, based on the method names, this EmailService.cfc handles a variety of emailing needs and circumstances.
Now, imagine that part of this eCommerce site has a shopping cart whose behavior is modeled by and encapsulated within another ColdFusion component: ShoppingCart.cfc. For the sake of exploration, let's assume that when the customer goes to checkout, the ShoppingCart.cfc ColdFusion component takes care of the order processing and sending out the order invoice. In order to send out the order invoice, the ShoppingCart.cfc needs a reference to the EmailService.cfc. However, the shopping cart doesn't need access to every possible email; in fact, the shopping cart only needs access to one of the email methods - sendInvoiceEmail().
In an effort to expose only the functionality that is required by the ShoppingCart.cfc component, we can use closures to create an on-the-fly proxy to the EmailService.cfc that only exposes the sendInvoiceEmail() method. By doing so, we limit the authority of the shopping cart to only the features that it is meant to have.
<cfscript>
// I return a new object with only the given method names
// exposed as behaviors on the resultant object. During the
// proxy creation, the method names can be renamed if a struct
// of names-pairs is passed in.
function exposeMethods( target, methods ){
// Create our method name mapping. In this collection, the
// key will be the original method name and the value will be
// the method name to be used in the proxy object.
var methodNameMap = {};
// Check to see if the methods collection is an array or a
// struct. If it's an array, then we want to convert it into
// a name-map with a direct translation so that we can treat
// the proxy creation in a uniform manner.
if (isArray( methods )){
// Convert each method into a direct mapping.
for (var methodName in methods){
methodNameMap[ methodName ] = methodName;
}
// If the methods collection is not an array, we will assume
// it is a struct.
} else {
// Simply use this struct as the name map.
methodNameMap = methods;
}
// Create the proxy object that we are going to return.
var proxy = {};
// Loop over each exposed method to create a named proxy
// method. We are going to be creating a new method for each
// method that we are proxying.
structEach(
methodNameMap,
function( originalMethodName, proxyMethodName ){
// Define the proxy method with the new name. This
// will simply pass the method message onto the
// target object.
proxy[ proxyMethodName ] = function(){
// Invoke the method on the taret object and
// return the result to the calling context.
return(
invoke( target, originalMethodName, arguments )
);
};
}
);
// Return the new proxy object with exposed behaviors.
return( proxy );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Create an instance of our email service.
emailService = new EmailService();
// Create an instance of our shopping cart. The shopping cart
// needs to be able to send out an email; BUT, it does not need
// access to all kinds of emails - just the one kind of email
// that will go out after a user has checked-out from shopping.
shoppingCart = new ShoppingCart(
emailService = exposeMethods(
emailService,
{
sendInvoiceEmail: "sendInvoice"
}
)
);
// ...
// ... user does some shopping. ... //
// ...
// Now that the user is done, let's process the order. This will
// process the order and send out an Invoice to the current
// customer using the composed EmailService instance.
shoppingCart.processOrder();
</cfscript>
In this experiment, not only am I creating a limited proxy to the EmailService.cfc, I am also renaming the sendInvoiceEmail() method during the translation. This was done mostly to demonstrate that these proxy objects can have an API that doesn't match the target object. Not only does this approach grant a minimal amount of authority, it also makes the coupling between the two components more flexible since this intermediary sandbox can easily absorb any name changes.
I'm not a good enough Application Architect to say whether this is a good approach or a bad approach - for me, this is just a fun exploration. I can say, however, that on the JavaScript side of things, I do see more and more "really smart people" using intermediary objects as a way to expose "sandboxed" communication between decoupled components. As such, I think it's definitely worth looking into a bit more.
Want to use code from this post? Check out the license.
Reader Comments
Very cool.
Now if I could just get it working by configuring in ColdSpring I could easily (further) secure several boundaries in an application... actually, I wonder if this might be a good way to 9re)implement the remoteMethodNames property of RemoteFactoryBean ...
@Tom,
Can't say that I really know anything about ColdSpring, but glad you found this interesting :)
@All,
Trying to take this concept one step further to use a "Sandbox" object as a way to further abstraction and enhance decoupling:
www.bennadel.com/blog/2363-Using-A-Sandbox-To-Decouple-ColdFusion-Components-In-A-Modular-Application-Architecture.htm
Now, I'm really far outside my comfort zone; but, interesting concepts.
Hey Ben!
What settings do you use for GIST?
-Rich
Wow like it ...really great.........that's my aspiration to be a blogger but ........i haven't reached there as yet .........congratulations Ben and friends
Ummm... I see the point, but only because there seems to be a flaw in the original design: Why create a single object responsible for sending dozens of different kinds of emails? What happens when I need to now send a shipping confirmation?
On the flip side, Apple frequently does something similar in Objective-C, when they create an interface file of selectors (methods) that only exposes the ones they want to be public.
@Michael,
Because then all your mail and template handling code is in one place.
The CFC is probably a Facade or something that knows how to orchestrate the templates and CFMAIL - logic best not scattered all over the app.
Tom
@Tom, I know it's a bit out of favor, but that's why you use inheritance.
If you have that much logic to manage, you create a master email cfc that knows how to do all of the things you mention, and then all of the individual kids inherit from it, perhaps overriding little things like the subject and body methods.
That approach is also better as it can help to eliminate the common logic you might have had in each individual mail method. Subclass with code that, say, knows how to iterate and display items in an order, and you have the parent for order confirmations, shipping notices, delayed or out of stock notices, and so on.
Put 'em all in a single "notifications" folder, so they can easily be found and edited, and you've solved your problem, old-school.