Browse DevX
Sign up for e-mail newsletters from DevX


A Design Pattern for Creating Reusable COM+ components using Visual Basic and XML : Page 2

Over the last year or two, quite a few articles and books have been written on COM+, transaction management, and programming stateful & stateless components, explaining in detail what COM+ does and how it does it. When tasked with creating a model for developing lightweight, reusable components, you can found a lot of What-not-How type of articles on COM+, but not a lot of practical How-To type of articles. The author started working through a lot of information on COM+, combined this information with how I wanted a reusable component to behave, and came up with a real easy model for creating reusable COM+ business objects.


Overview: How COM+ manages transactions & state

I will attempt to give a quick overview of how COM+ goes about its business. Quite a few articles and books are available on the topic by acclaimed authors.  I can recommend the book Programming Distributed Applications with COM+ and Microsoft Visual Basic 6 by Ted Pattison, or an article he wrote for Microsoft Systems Journal in October 1999, titled Writing COM+-style Transactions with Visual Basic and SQL Server.  The following overview provides a very brief summary of the key functionality of COM+.  Understanding the impact of each of these functions plays a crucial role in understanding how to use them in your reusable objects.

COM+ provides a programming model based on declarative transactions.  Setting the MTSTransactionMode property of the class sets an object’s transaction mode at design time.  When the COM+ runtime creates a new object, it examines the component’s MTSTransactionMode setting to determine whether the new object should be created inside a transaction.  Once an object has been created, you cannot add it to a transaction, neither can you disassociate it from the transaction in which it has been created.  Once an object is created inside a transaction, it spends its entire lifetime inside this transaction.  When the transaction is committed or aborted, the COM+ runtime destroys all the objects inside it.

When the COM+ runtime receives a request to create a new object, it inspects the transaction context of the creator to determine whether the creator is running inside a transaction.  It also inspects the transaction support attribute of the new object.  Using these two pieces of information it decides whether to create the object 1) inside a new transaction, 2) inside the same transaction as its creator or 3) without a transaction. 

Figure 4 shows a diagram of a client application connecting to an object (the root object) through a COM+ context.  The root object in turn instantiated a secondary object in the same COM+ transaction.  The root object connects to the secondary object through another COM+ context.  Figure 4 also shows three important Boolean values that COM+ uses to manage the scope of the transaction and the object lifetime.  In Figure 4 these flags are set to their default values.  For a detailed discussion on how COM+ uses this three flags, refer to chapter 10 of the book by Ted Pattison mentioned above.

Figure 4: Root object and Secondary object in COM+ transaction

The root COM+ object plays a very important role in a COM+ transaction. Every transaction as a whole maintains a Boolean value (MustAbort) that indicates whether the transaction should be aborted.  This value is initially set to False when the transaction is created.  COM+ inspects the value of MustAbort when:

  • The root object returns control to its caller.  If MustAbort is False nothing happens.  If it is True, COM+ aborts the transaction and deactivates all the secondary objects inside the transaction.
  • The root object is deactivated. 

Depending on the value of MustAbort, COM+ will then either commit or abort the transaction.  Whatever the outcome of the transaction, when the transaction completes, COM+  will recycle the object(s) that took part in the transaction.  This means that the lifetime of the transaction plays an enormous part in designing the interaction between objects partaking in either the same or different transactions.

There are two method pairs available to the developer with which to control the transaction scope and outcome.  These are DisableCommit/Enablecommit and SetComplete/SetAbort.

The DisableCommit and EnableCommit methods allows you to set the value of the IsHappy flag.  When an object running inside a transaction is deactivated, and IsHappy is False, the transaction’s MustAbort flag is set to true, which will cause the transaction to be aborted.  Note that this is method only sets the value of IsHappy, it does not perform any other logic.  This means that the value of this flag can be changed multiple times, but the value is only taken into account when the object is deactivated.

The SetComplete and SetAbort methods sets the value of IsHappy to either True or False, plus it sets the value of IsDone to True.  The value of the IsDone flag is examined every time an object returns control to its caller.  If the IsDone value is True, COM+ deactivates this object, and the normal process to determine whether to commit or abort the transaction is started.

From the above it is clear that, if you are developing an object that may be reused by other objects on the BO level, care has to be taken when calling SetComplete and SetAbort.  If your object participates in a transaction as a secondary object, and SetComplete or SetAbort is called, this object will be recycled (and the instance state will be lost) when it returns control to the calling object.  This may not be what the calling object was expecting.  An example of this is: In Object B, the Save method does a SetComplete at successful completion.  Object A uses an instance of Object B it loads an instance of Object B, does some work and saves, it does some more work and want to save it again.  Problem is that after the first time Save is executed, Object B is recycled when it returns control to Object A, and the instance state is lost.  Object A is not aware of this, as it actually connects to the context and not directly to Object B.  The next time Object A tries to access any method or property of Object B, a new instance of Object B is created within the existing object context.  Object A is totally unaware that it is now working with another instance of Object B.  If Object A now performs logic that is dependant on the state of Object B, it will produce erroneous results.

Issues such as the above means that you cannot call SetComplete unless you are absolutely sure that no other object expects you to hold your state.  As your component will be reused on a binary level, it has no guarantee who uses it or exactly how it is being used.  This means that great care has to be taken when calling SetComplete in your component, and that you have to inform any developer of another BO-level calling component of this fact, at design time.  You can alert the user of your component to the fact that you are using SetComplete in a method by documentation or a naming convention.

Any BO level component should always only be accessed by its CO counterpart component from the CO level. Because a component and its CO level partner will always be designed and implemented together, the CO object can take the usage of SetComplete into account in the design stage. 

When designing interaction between COM+ objects, I make use the following rules (which I like to refer to as the COM+ State Rules) :

  1. Non Transactional COM+ objects can maintain instance state
  2. Transactional COM+ objects maintains instance state for the duration of the transaction
  3. Transactional COM+ objects cannot maintain instance state across transaction boundaries
  4. Transaction scope can be managed by object scope
  5. Transaction scope can be managed by calling SetComplete/SetAbort
  6. Acquire resources late, release them early

Method Explained

The core concept behind the method I follow is to realize other BO objects as the primary users of your reusable BO object, with the BO object’s own CO counterpart seen as a secondary user of the object.  This is quite a significant paradigm shift from seeing the CO object as the primary user of its BO counterpart.  When the CO object is seen as the primary user, all methods on the BO object is normally geared towards a) getting state information from the CO, b) performing an action on this state and c) returning the changed state to the CO.  When you view other BO objects as the primary users of your BO level object, the interfaces to the functionality provided by the object changes from simple Get-In, Do-your-stuff and Return-Results type of approach, to a full OOP interface. The BO object needs to expose its methods and properties via an OOP interface to other BO objects.  All the functionality available to other BO level objects must however also be available to the object’s own CO counterpart.  There must exist only one set of business logic on the BO layer, accessible to the BO and the CO layer in the following ways:

  1. A BO object (Object A) can instantiate an instance of another BO object (Object B).  Object B exposes an OOP interface to Object A through which Object A is able to call methods and set and retrieve properties of Object B.  Object A can control the scope of the instance of Object B and by doing so, the scope of the transaction
  2. A CO object is able to call the business logic in its corresponding BO object via a wrapper function.  It calls a function on the BO object and passes its instance state into this function in one or more state variables.  The function executes the same method exposed to other BO level objects, and when it is done, the CO can retrieve the changed instance state from the state variables.  The CO object does not expect the Bo object to maintain state between method calls, and sends and retrieves the instance state to and from the BO object with every function call.

The above requirements are met by following a very simple recipe on the BO object.  The functionality on BO level is provided in plain object oriented fashion.  The BO object has property procedures with private variables to maintain the instance state, and functionality is provided as methods that act on the properties of the object (and any secondary objects).  Any BO object is trusted to never call SetComplete when accessed by other BO objects (We will examine the use of SetAbort in more detail when we look at error handling).  If you have a look at the COM+ State Rules, you will see that a root object then only has to worry about the scope of its secondary objects, as object instance state and transaction management is done using the functionality provided by COM+.

CO access to the methods on the BO is provided by a wrapper function on the BO object.  Note that I refer to it as a wrapper function and not a method.  This is purely a naming convention I use to distinguish between the methods exposed for use by other BO objects (called methods), and the methods exposed for use by CO objects (I call these functions).  The reason I refer to this specific type of method as a function is because it receives all the information it needs via the parameter list, as opposed to a normal method that acts on the existing state of an object.  So, this wrapper function is implemented as a normal method of the object, but it takes as input a variable or variables carrying the total instance state of the object.  This wrapper function is very simple, as it performs a maximum of three tasks:

  1. The instance state as sent from the CO object is copied into the local state variables of the BO object

  2. The corresponding method is executed

  3. The local instance state is copied from the local variables into the By Reference variables from where the CO object will have access to them  

Depending on the logic in the method, step 1 or 3 may be left out.  If the method is used to retrieve instance state from the database, step 1 may be left out, while step 3 may be left out if the method does not make any changes to the instance state of the object.

On the CO object, the methods that call the functions on the BO object follow a similar pattern. The methods generally consist of the following steps

  1. The local instance state is packed into a variable
  2. An instance of the BO object is created
  3. The BO function is called and the local instance state is passed to this function
  4. The BO object is destroyed
  5. The instance state is unpacked into the local variables

Depending on the logic of the method executed, it may not be necessary to send state to the BO method (when loading the object from the database for example), or to retrieve state back from the method (when deleting the object from the database)

So, for every method on the BO object, there is a function call to expose this method to its CO object.  In order to distinguish between the function and the method, I use the following naming convention: The method and the function gets the same name, with the prefix f_ added to the start of the name of the function, indicating that this is the wrapper function and only to be called from the CO level.

Thanks for your registration, follow us on our social networks to keep up-to-date