Simplify MTS Apps with a Design PatternUse a design pattern to reduce transactional overheadin MTS and extend the life of your apps. by Mike Alvarez Reprinted with permission from Visual Basic Programmer's Journal, Feb 1999, Volume 9, Issue 2, Copyright 1999, Fawcette Technical Publications, Palo Alto, CA, USA. To subscribe, call 1-800-848-5523, 650-833-7100, visit www.vbpj.com, or visit The Development Exchange. Why do you need a design pattern? You don't. Unless, that is, you value code reuse, you want to deliver solutions more quickly, and you want to extend the life expectancy of your apps. Using a design pattern to implement a VB-and-MTS solution can make this a reality. For example, generalizing your components so they more closely match their Create, Read, Update, and Delete (CRUD) responsibilities can make transactional and state requirements easier to deal with. It also enables you to spend your time adding functionality rather than working on your app's plumbing.
As an example, I'll walk you through the process of designing an online order-taking app for a pet store. I won't show you how to create the individual components of the appthere isn't space for that. Instead, I'll explain something more critical: how to use this design pattern to get better performance in MTS and make your systems last longer when the time for a redesign eventually arrives. I'll use VB5 with MTS 2.0, but you can also implement this design using VB6 and MTS 2.0. My main goal: to show you how to categorize your business-tier objects and decompose the most dynamic types of objects (Business Logic objects) so they fit nicely into the MTS and distributed-application world. When you develop component-based, multitier apps, you end up with many different kinds of objects. In most cases, these objects fall into one of three categories: Business Logic, Framework, or System Service objects (see Figure 1). Business Logic objects are highly customized to the needs of your business. They provide little in terms of code reuse from project to project because you tailor these specifically to your customer's needs. This is where most of your business logic resides. Framework objects represent the business functions you find in most companies, but aren't sufficiently unique to obtain binary-level reuse. Finally, System Service objects serve the other objects of your business tier. You can often achieve binary-level reuse in this category because these objects perform largely static services. These objects include data access layers, logging, and security services.
Before tackling the design pattern itself, take a careful look at the business problem. Start by defining the functionality the store needs, then use this information to decide which business objects the app requires. A customer logs onto the pet store's Web site, enters information about a pet he or she wants to purchase, then clicks on a Search button. This produces a list of matching animals and their prices. The user can then choose an animal and click on a Continue button, causing the app to display an outline of the transaction details, including price and delivery options. The user then enters his or her credit card number into a textbox on the screen. Finally, the user clicks on the Continue button again, and the app completes the transaction. The user now owns a new pet of his or her choice. Analyzing the required functionality points out the need for three objects: Animal, Customer, and Bill. Assuming you don't know which data access direction you want to take, you need to include a data access layer. Finally, you want to give the components a centralized way of logging, so you need to build a logging object. The objects cited fit into the three business-tier categories nicely. The Customer and Animal objects fall into the Business Logic object category, the Bill object falls into the Framework object category, and the data access layer object (DataBroker) and the logging object (LogIt) fall into the System Service object category. Next, implement all the methods for each object on that particular object. For example, implement all the Animal methods on an Animal object. This is not the best approach, but it is the most common, and it serves as a good comparison for the design pattern I explain in this article. The needs of this system dictate two methods on the Animal object: FindAnimal() and BuyAnimal(). FindAnimal() finds an animal the customer chooses based on some search criteria; BuyAnimal() transfers ownership of a selected animal. The Customer object isn't widely used in this example, but it does include a method called UpdatePurchaseCount(). This method tracks the number and types of purchases a customer makes on the system. Finally, the Bill object requires a BillMe() method, which takes the customer's financial information and writes a database record you use to charge the customer's credit card at a later time. Minimize Client Round Trips You can deploy these objects under MTS on any machine you choose, but you should not assume that all the objects will remain in the same package or even on the same machine. So, you need to minimize the number of round trips from the client and do as much as you can in one method call. This doesn't mean you should implement everything in one method call or completely sacrifice your OO design, but you need to strike a balance. For the sake of simplicity, omit the methods on the DataBroker and LogIt objects, but note that the DataBroker object contains methods that allow a business-tier component to perform queries, updates, inserts, and deletes on a database. The LogIt object includes a method for providing logging services to other objects. Next, look at the Pets Online example from a technical point of view. The first ASP page displays a form that allows the user to enter search criteria for an animal he or she wants to buy. The customer enters his or her specific animal requirements, and the app submits the form's data to the FindAnimal() method of the Animal object, which in turn calls the DataBroker object to handle the database request. The FindAnimal() method returns a matching list of animals to the initial ASP page. This page hands the list off to a second ASP page. The second ASP page enables the user to select the animal he or she wants to buy. The page also provides a form for entering the customer's financial information. The customer fills out and submits this form; the ASP page then calls the Animal object's BuyAnimal() method. The BuyAnimal method calls the DataBroker object, which updates the animal's record to indicate it has been purchased. The method then calls the Customer object's UpdatePurchaseCount() method, which calls the DataBroker object. After this call returns, the BuyAnimal() method calls the Bill object's BillMe() method, which makes the final call to the DataBroker object. The customer's final billing information is returned and displayed on the final ASP page. The LogIt object records checkpoint information throughout the process. The actions the BuyAnimal() method initiates are transactional and spawn three separate database writes. This means you must mark the Animal and Customer objects as "Requires a Transaction" under MTS. You could mark the Customer object as "Supports Transactions," but the object also includes a method called AddCustomer(), which requires a transaction. Mark the Bill and DataBroker objects as Supports Transactions. This enables the Animal object to initiate the transaction and have the Customer, Bill, and DataBroker objects all take part in the transaction. If a database write fails when you try to bill a customer, MTS rolls back both the Customer update and the Animal update. What's Wrong With This Approach? Generalizing your objects enables you to see them in terms of CRUD actions. The online pet store example is relatively simple, so you have only one or two actions on each object. A real-world, enterprise object might have many CRUD actions; such an object could have several stateless search methods along with a few Create, Update, and Delete methods that might or might not be stateless. This approach seems to work, so what's the problem? Actually, there are several. First, the Customer and Animal objects perform transactional searches because you mark them as Requires a Transaction. It doesn't make sense to mark Read methods as transactional and incur the transactional overhead. However, you have no choice in this approach because you can mark only components as having transactional attributes, not methods. Another possible problem: The methods return client-friendly errors in the system-provided error object. This is great for returning errors to the client because you can return the error number, description, and source in one object. However, it is better to return some type of status when business-tier objects call one another than to return an error object because the latter approach breaks you out of your processing flow. Another problem crops up when you store state. MTS reclaims the state of Animal and Customer objects when you call SetAbort and SetComplete. You can still store state when you have all your methods in the same class, but you cannot call SetAbort or SetComplete in a lower-level function because MTS reclaims the state in the high-level function when the function returns to the client. The inability to call SetAbort and SetComplete in lower-level functions makes it more difficult to take advantage of Just in Time (JIT) activation, As Soon As Possible (ASAP) deactivation, and state storage.
Decompose Your Objects The Manager component provides the main interface to the client. It exposes the methods the client requires and delegates them to either the Search or Maintenance object. You use the Manager object because you must separate the transactional objects from the nontransactional ones. Splitting your objects into Search and Maintenance only would force the client to determine which object to instantiate based on the action it requires. The manager's main responsibility is to act as the interface object to the client. It also acts as the state manager for the entire collection of objects. In other words, the AnimalManager object is the appropriate place to store state for the Animal object. The Search object conducts search-type queries for the object in question. For example, the AnimalSearch object contains methods that ascertain specific information about an animal. This enables you to perform search queries on the database without incurring any transactional overhead. The statelessness of the Search object gives you maximum scalability. You put the rest of the CRUD services (all except Read) in the Maintenance object. Maintenance objects prove the most complex in the model because you implement most of your business rules in them. You almost always mark these objects as Requires a Transaction under MTS. You don't decompose Framework and System Service objects the same way you decompose Business Logic objects because Framework and System Service objects make it easy to decide whether to mark them as transactional. For example, a Bill object always supports transactions, so you always mark it as Supports Transactions. On the other hand, a Fee object that looks up information on fees for other objects in your system does not perform any transactions. You never have to mark it as Supports Transactions.
That's about it. Separating the CRUD objects into three separate objects (Manager, Search, and Maintenance) enables you to eliminate much of the wasted overhead for using MTS because you mark only the objects that require transaction support (Maintenance) as requiring it. This also solves the problem of returning client-friendly errors to another business-tier object because Search and Maintenance objects return their status to the Manager object. The Manager object then turns that status into a client-friendly error. So, Search and Maintenance objects receive a status rather than an error object when they talk directly to each other. Finally, splitting apart the CRUD objects helps you clear up the state problem as well, because the manager assumes responsibility for storing state. MTS can reclaim Search and Maintenance objects, but state is maintained as long as the client holds the reference to the Manager object.
|