21 July 2008
Read the previous articles in the series, starting with Developing Flex RIAs with Cairngorm Microarchitecture – Part 1: Introducing Cairngorm before reading Part 6.
Beginning
Throughout the series you will find references to code taken from an e-commerce application named Cairngorm Store. You may use this sample application to gain a better understanding of Cairngorm, but please consider it only as a guide. Adobe is not responsible for maintaining the Cairngorm Store application.
To begin the final article of this series, take a moment to stand on the summit of Cairngorm and look down at how far you have climbed.
With a deeper understanding of Cairngorm, you now have the knowledge, tools, and expertise to be able to scale any complex rich Internet application, applying the lessons you have learned to tackle any application development project confidently, no matter how steep the problem may first appear.
By applying these lessons, you can consistently, effectively, and rapidly accomplish the design, development, and delivery of rich Internet applications knowing that the methods you use have been proven effective on numerous challenges as complex and critical as your own.
Let's consolidate this understanding with a quick review of the different patterns in Cairngorm and how they work together. In this article, we will review the essential infrastructure that you must have in place to efficiently deliver feature after feature into your application.
In addition, we'll show you how to complete a new development task in the Cairngorm Store. You can use the exercise to confirm that you are now ready to develop your own applications with Cairngorm by correctly applying the same best practices that so many other Flex developers use with success.
To start an end-to-end review of the Cairngorm architecture, remember that Cairngorm is designed to facilitate a rapport between man and machine, listening to what the user wants, ensuring that the user gets what he or she wants, and then communicating that back to the user.
The user is in charge of the conversation. Your rich Internet application must awaits some indication from the user to know what to do. These indications—pressing buttons, dragging and dropping icons, double-clicking rows or submitting forms—are all called "user gestures."
Cairngorm translates these user gestures into Cairngorm events. Whenever there is a click, press, drag, drop, or submission of any event that represents a user demand, you can broadcast the event by using either the built in dispatch() method or by using CairngormEventDispatcher. The event broadcast is the beginning of the Cairngorm conversation.
The Front Controller pattern is the sole listener to Cairngorm events. By centralizing the handling of events, the Front Controller assumes responsibility for meeting the user's demands.
However, the Front Controller doesn't do the work; it's a manager, not a worker. The Front Controller instead maintains a list of "who does what," a list of commands that are best suited to responding to particular events.
Once the Front Controller recognizes an event for which it has a command on the payroll, as it were, it tells that command to execute. The Front Controller tells each and every command what to do in a consistent manner: the Front Controller calls the execute() method on the command and the command does its specific work.
Some commands require the server to perform a duty. In a rich Internet application, some business logic executes on the client side. More often than not, however, you'll want to shift some of the responsibility for business logic to the server.
Commands don't care about how something gets done; they just care that it gets done. Commands prefer to delegate the responsibility for server-side business logic to another class. There is a good chance, after all, that several commands that are trying to achieve distinct goals of their own will all require the same service for different things. What's more, for all the command knows, the business logic hasn't even been written on the server yet. Maybe the command is simply calling a dummy Business Delegate class, where the server-side infrastructure does not yet exist, but the Business Delegate offers an interface that will exist in the future.
So whenever you have business logic, put it into a Business Delegate class so that your commands can invoke methods on it.
The Business Delegate provides a seamless interface between commands and any services on the server. These services may be Java services, LiveCycle Data Services, ColdFusion Components (CFCs), web services, or simple HTTP services. The Business Delegate treats these services just the same, invoking them when the command asks and handing results to the command immediately when they become available.
However, the implementation details of these services—the names of the Java classes or CFCs, destinations, location of WSDL files, or the URLs for HTTP services—are all encapsulated into a shared repository of services, called the Service Locator.
The Service Locator is a single directory of all services that your rich Internet application requires. Because only the Business Delegate has responsibility for calling services, only the Business Delegate uses the Service Locator to locate services by name (caring nothing of their implementation) before invoking these services and handing the results to the Command classes.
Remember, any Command classes that assume the responsibility of responding to service calls and taking the returned data from the Business Delegate must do so by implementing the IResponder interface. In this way, the Business Delegate knows that the command will have a result() method to handle all results and a fault() method to handle any errors.
What happens to the data that is passed between client and server, and held on the client? Cairngorm insists that you treat data with a little respect, encapsulating it in Value Objects (also known as Data Transfer Objects) that describe what the data is in business terms ("it's an Order") rather than in technical terms ("it's an ArrayList of Objects containing a String and a Number").
Finally, your application must be able to communicate back to the user. In Cairngorm, the Model Locator offers a pattern to store state, whether for the entire application, or part of the application. Developers have the option to have one or more models in their application to manage client-side state. Though Cairngorm does not prescribe how to implement the user experience with MXML and ActionScript, it does insist that anywhere the view is dynamic that the data for this view uses data binding to a Model Locator implementation.
Client-side state held within the Model Locator should be stored as Value Objects. When your Command classes ask the Business Delegate to call some server-side business logic on your user's behalf, they implement the IResponder interface and receive either Value Objects or collections of Value Objects in the result() method.
The very last part of the conversation in a Cairngorm application results in the command updating the Model Locator with any new state represented by these Value Objects. With the Model Locator using data binding to notify the view, the user interface updates to display any new data and completes the conversation with the user.
The previous sections describe the flow of control through the various design patterns in a Cairngorm architecture. Each and every feature in an application traces the same flow of execution, with user gestures becoming events, events becoming service calls, and data passing back into the model.
When you add a new feature to a Cairngorm application, it is as simple as the following process:
addCommand().execute() method to do the work.result() method to handle any results from the server:Additionally, you will have to create any Value Objects that are required to pass data between the command and Business Delegate, between the Business Delegate and the server, or to store data on the Model Locator. Typically, however, your Value Objects will exist early in the development of your application and you will normally reuse them as you add new features to your application.
Similarly, as your application grows, the server-side services quickly become locked in place and application development proceeds even more quickly, following a simplified process:
addCommand().That's really all there is to it.
Debugging a Cairngorm application involves the same predictable steps. If a desired feature doesn't perform in response to a user gesture, the debug cycle is always the same:
execute() method is called on the command by the Controller.result() method is called in the command.With these five steps, it's painless to isolate a problem in an application and fix it.
One feature often found in e-commerce applications is a "wish list," a temporary list of products that the user might want to purchase in the future without adding them to the shopping cart now.
In an agile development team, the requirement or "story" for a Wish List in Cairngorm Store might read as follows:
Cairngorm Store Wish List
In addition to being able to add items from the store into their Shopping Basket, customers could use a Wish List feature to track items that they wish to purchase in the future.
Using a tab-interface, the customer will be able to choose either a Shopping Basket or a Wish List as the place where they place items. Items placed in the Wish List do not appear in the checkout when the customer proceeds to purchase the items in their shopping basket.
The customer will be able to add items to their Wish List either by dragging a product's image or description from a product list onto the Wish List, by dragging the image from the product details panel onto the Wish List, or by pressing an Add to Wish List button from the product details panel.
Note that this requirement doesn't dictate how the customer might move objects between the Wish List and the checkout. Let's assume for now that this requirement is an additional story.
The tabbed interface was chosen for demonstration purposes. In a real-world project, we would use the capabilities of a user experience consultant to incorporate the Wish List seamlessly and effectively into the overall information architecture!
In an agile development environment, a developer usually breaks a story down into tasks. With a Cairngorm microarchitecture in place, the tasks on any given feature become quite predictable and repeatable, as noted in the feature-driven development discussion in Part 4 of this article series.
For the Wish List story above, we break down the tasks in the story as follows:
addProductToWishList event with the Front Controller class.AddProductToWishList command to handle the event.WishList object that resides within ModelLocator.WishList object into a Tab Navigator control next to the Shopping Cart, to allow viewing of the wish list.WishList object to ModelLocator.FrontController when the user presses the Add to Wish List button.The first step is to indicate to the Cairngorm-based application that there is a new user gesture that will occur when the user wants to add products to their wish list.
Start by creating a new Cairngorm Event that will later be added to the Front Controller.
package com.adobe.cairngorm.samples.store.event
{
import com.adobe.cairngorm.control.CairngormEvent;
public class AddProductToWishListEvent extends CairngormEvent
{
public static var ADD_PRODUCT_TO_WISHLIST : String = "addProductToWishList";
public var product : ProductVO;
public var quantity : Number;
public function FilterProductsEvent( product:ProductVO, quantity:Number )
{
this.product = product;
this.quantity = quantity;
}
}
Remember, by using a static constant to name the events, you check the validity of broadcasted events at compile-time. This ensures that your application does not fail silently when you inadvertently misspell the name of the event before broadcasting it into a black-hole.
Next, you must register a Command class that will assume responsibility for carrying out the work associated with this event. The Cairngorm Store has its own Front Controller instance, ShopController.as, which you use to register all the appropriate Cairngorm Store commands. So add a new entry to the initialiseCommands() method as follows:
addCommand(AddProductToWishListEvent. ADD_PRODUCT_TO_WISHLIST , AddProductToWishListCommand );
That's it as far as the Front Controller class instance is concerned. Now it knows of the new event, and it knows what to do when the event occurs.
So next, you have to create the Command class that you have delegated as responsible for this event: the AddProductToWishListCommand class.
The code for the new command class is simple and self-explanatory:
package com.adobe.cairngorm.samples.store.command
{
import com.adobe.cairngorm.commands.ICommand;
import com.adobe.cairngorm.control.CairngormEvent;
import com.adobe.cairngorm.samples.store.model.ShopModelLocator;
import com.adobe.cairngorm.samples.store.event.AddProductToWishListEvent;
public class AddProductToWishListCommand implements ICommand
{
public function AddProductToWishListCommand() {}
public function execute( event:CairngormEvent ) : void
{
var addEvent : AddProductToWishListEvent =
AddProductToWishListEvent( event );
ShopModelLocator.getInstance().wishlist.addProduct(
addEvent.product, addEvent.quantity );
}
}
}
There's nothing complex at all about this code. The execute() method is the command-independent entry point that the Front Controller calls whenever an AddProductToWishListEvent event occurs.
The execute() method simply adds the appropriate quantity of the product involved in the user's request to "Add to wish list" to the WishList object held on ShopModelLocator.
And again, that's it. You can assume for now that the user interface (the view) is bound through Flex data binding to the WishList object held on ShopModelLocator. That's all there is to do in terms of business logic!
The next step is to ensure that you have a WishList object on ShopModelLocator.
Cairngorm makes Flex development embarrassingly simple from this point forward. Since you already have a ShoppingCart class in Cairngorm Store that maintains a list of products and quantities of those products, you can simply reuse this class to implement the WishList object. A WishList object is after all, simply a Shopping Cart that you don't necessarily want to take to the checkout on this particular visit.
Quite simply, you create a new instance of the ShoppingCart class and call it wishList instance. You create the new instance in ShopModelLocator's constructor, as follows:
ShopModelLocator.wishList = new ShoppingCart();
This assumes that you have declared the wishList instance alongside the other definitions in ShopModelLocator, as follows:
public var wishList : ShoppingCart;
That's really all there is to it. Are you getting a sense of just how rapidly you can add new features to a Cairngorm application?
You have have created a Command class, registered it with the Front Controller class, ensured it updates the Model Locator class with products added to the wishList, and you've reused a business object (ShoppingCart) to implement the wish list.
All that remains is creating the user interface for the wish list to ensure that it is dynamically bound to ShopModelLocator for the presentation of dynamic data, and to ensure that any user gestures that occur on the interface result in the AddProductToWishListEvent event being broadcast.
First off, let's consider how to create the view itself. By replacing the area of screen that only shows the ShoppingCart MXML component with a TabNavigator, you can hold two instances of the ShoppingCart. The first of these instances is the original shopping cart, while the second will be the wish list.
Next you need to alter the ProductDetails component to capture the user gesture to add a product to the wish list, as well as adding a product to the shopping cart. This will require adding an addProductToWishList event to the ProductDetails MXML component, as shown in the highlighted code below.
<details:ProductDetails id="productDetailsComp" width="100%" height="325" currencyFormatter="{ ShopModelLocator.currencyFormatter }"
selectedItem="{ ShopModelLocator.selectedItem }" addProduct="addProductToShoppingCart( event )" addProductToWishList="addProductToWishList( event )" />
<mx:TabNavigator width="100%" height="100%">
<!-- the Shopping Cart -->
<cart:ShoppingCart id="shoppingCartComp" label="Shopping Cart" width="100%" height="100%" shoppingCart="{ ShopModelLocator.shoppingCart }"
selectedItem="{ ShopModelLocator.selectedItem }" select=" ShopModelLocator.selectedItem = event.target.selectedItem"
currencyFormatter="{ ShopModelLocator.currencyFormatter }" addProduct="addProductToShoppingCart( event );"
deleteProduct="deleteProductFromShoppingCart( event );"
gotoCheckout="ShopModelLocator.workflowState = ShopModelLocator.VIEWING_CHECKOUT;" />
<!-- the Wish List -->
<cart:ShoppingCart id="wishListCartComp" label="Wish List" width="100%" height="100%"
shoppingCart="{ ShopModelLocator.wishList }"
selectedItem="{ ShopModelLocator.selectedItem }"
select=" ShopModelLocator.selectedItem = event.target.selectedItem"
currencyFormatter="{ ShopModelLocator.currencyFormatter }"
addProduct="addProductToWishList( event );"
deleteProduct="deleteProductFromShoppingCart( event );"
gotoCheckout="ShopModelLocator.workflowState = ShopModelLocator.VIEWING_CHECKOUT;" />
</mx:TabNavigator>
Start by looking at the ShoppingCart instance called wishListCartComp in the Tab Navigator control.
Because the wishListCartComp instance data binds to ShopModelLocator.wishList, any changes to the wish list model are automatically taken care of by the internals of the Shopping Cart component. That's a tremendous example of component reuse in action.
The addProduct event handler invokes a method, addProductToWishList(), which you need to define prior to the view declarations.
private function addProductToWishList( event:AddProductEvent ) : void
{
var event:AddProductEvent = AddProductEvent( event );
new AddProductToWishListEvent( event.product, event.quantity).dispatch()
;
}
This method is responsible for translating the user gesture into the appropriate Cairngorm event. Now, when the user drops a product onto the wishListCartComp instance of the shopping cart component, it is recognized as a user gesture to add something to the Wish List, and this gesture is translated into the appropriate Cairngorm event.
You re-use this addProductToWishList() method in the ProductDetails MXML component, as shown in this line of code:
addProductToWishList="addProductToWishList( event )"
This highlights another best practice. The event handler addProductToWishList() receives an event of type AddProductEvent. This is an event class that you create, not only to wrap data sent via an event, but in a package that represents your business domain, rather than a collection of strings and numbers belonging to an untyped object. It is not a CairngormEvent, but a sub class of Event from the Flex framework. Consider the code below:
package com.adobe.cairngorm.samples.store.event
{
import flash.events.Event;
public class AddProductEvent extends Event
{
public static const ADD_PRODUCT : String = "addProduct";
public var product:ProductVO;
public var quantity:Number;
public function AddProductEvent()
{
super( ADD_PRODUCT );
}
}
}
The wish list view can already dispatch this event as shown above. The ProductDetails view, however, clearly needs to be given this ability. You must ensure the component can broadcast out an addProductToWishList event as the result of a click event happening on a new button "Add to Wish List" that is placed inside it. So the final piece of view code required is in the following additions to ProductDetails.mxml.
First, update the definition of MetaData on ProductDetails to advertise the new event you can broadcast, as shown by the additional highlighted line of code below:
<mx:Metadata>
[Event(name="addProduct", type="com.adobe.cairngorm.samples.store.event.AddProductEvent")]
[Event(name="addProductToWishList", type="com.adobe.cairngorm.samples.store.event.AddProductEvent")]
</mx:Metadata>
Finally, declare the new button and mutate its click event into a more meaningful business event, addProductToWishList. Declare a utility method that broadcasts the addProductToWishList event with some additional information: the product to be added (the currently selected item) and the quantity of that product the user has indicated he or she wishes to add:
private
function addProductToWishList() : void
{
var event:AddProductEvent = new AddProductEvent();
event.product = selectedItem;
event.quantity = quantity;
dispatchEvent( event );
}
You can then add a new button next to the "Add To Shopping Cart" button, to instead add to the wish list, as shown in the highlighted code below:
<mx:Button label="Add to Cart" click="addProduct();" />
<mx:Button label="Add to WishList" click="addProductToWishList();" />
This demonstrates one of the best practices mentioned in the third article in the series: taking a click event on a button, turning it into a more meaningful event that represents your business domain (AddProductEvent as "a ddProductToWishList"), and loosely coupling the MXML components—in this case ProductDetails and its parent MXML components through the broadcasting and handling of these events.
And that's it. Now you've added all the code to the MXML that represents the user interface for the wish list. By dynamically binding this MXML to the Model Locator class and by translating user gestures into Cairngorm events, the view is weaved into the fabric of the underlying Cairngorm architecture.
We hope that this final article has sealed a journey of understanding and clarified the benefits of building rich Internet applications on the Cairngorm microarchitecture. The benefits demonstrate themselves very quickly as the application scales in terms of the number of features in the product you are developing.
Furthermore, we would trust that you now recognize the predictability and consistency in how a Cairngorm-aware developer chooses to decompose a business requirement into technical tasks that can be implemented upon the Cairngorm microarchitecture. It is my experience that such consistency and predictability yields many benefits over and above elegance of code.
On Cairngorm-based projects, we find that developers are able to more accurately and consistently estimate the cost and effort involved in implementing a particular feature by decomposing into consistent tasks, and having many previous yardsticks against which to measure.
In my experience that with a Cairngorm microarchitecture, developers are much more able to embrace the concept of a "collective ownership," where a developer is no longer a specialist in one specific area of the application, but is instead a generalist that can implement features across the breadth of an application. Aside from addressing the "what if one of your developers is knocked over by a bus" dilemma, Adobe Consulting has found that generalization means that the skill level of a team develops at a much greater rate than if developers become specialists in only a small subset of the product and the technology platform.
There are many other benefits to gain from an engineering perspective, but if you are with me this far, we trust you are already realizing the benefits for yourself. Most importantly, we trust that you have recognized that standing on the summit of Cairngorm, it's not actually very far down to the bottom.
It's important to reiterate that Cairngorm isn't the only way to build rich Internet applications. It is, however, a proven way of delivering rich Internet applications into production. From that perspective, it exemplifies best practices from the perspective of the numerous organizations, teams, and individuals who are working with Flex and Cairngorm together.
If you choose to embrace Cairngorm, or even just the concepts and ideas we have presented through Cairngorm into your own rich Internet application development, then we am confident your delivery will be all the more assured because of it.
We look forward to seeing your great work become even greater user experiences.
Cairngorm continues to evolve with the Flex platform.
Cairngorm 2.2 is now available and Adobe Consulting will continue to evolve Cairngorm as experiences grow and to accommodate updates to the Flex framework. For more details, visit the home of Cairngorm on Adobe Labs here http://labs.adobe.com/wiki/index.php/Cairngorm.
Adobe Consulting is committed to continuing to invest in the support of the Cairngorm platform, blending real-world field experience with the Flex product engineers and community members who are actively engaged in the Cairngorm committee.
The active members of the Cairngorm committee are all regular contributors to the Flexcoders mailing list, where you can ask any of your Flex and Cairngorm-related questions.
For those interested in how Cairngorm has already been implemented, and how we solved problems from the beginning, see the book Developing Rich Clients with Macromedia Flex by Steven Webster and Alistair McLeod, which covers many of the original concepts behind Cairngorm in detail.