Planning Your Approach to DCOM

by Steve Francis

It's no secret that a well-designed object model will result in faster and cleaner front-end development. Running the business-logic layer on a separate box provides such benefits as centralized administration, better scalability, easier deployment, and overall memory conservation.

With Microsoft's current COM model, it would seem that the benefits of these two approaches are at odds with each other. An unplanned marriage of the two approaches will result in a clunky application that can choke even the fastest networks.

In this article, we'll explain how to maintain a "purer" object model and, at the same time, run the business server remotely—all while keeping network traffic under control. First, however, let's look at an alternative.

Microsoft Transaction Server

Microsoft Transaction Server (MTS) is a good product offering many tangible benefits, including connection pooling, object pooling (coming soon), and tight transaction control. While these benefits add up to better resource management on the server, they result in poorer resource management on the client. The fatter client is because of Transaction Server's preference for stateless objects.

Other less attractive aspects of MTS include maintaining huge method/function prototypes and maintenance-intensive code, on either the client or the server, to deal with this situation. Thus, many benefits of an object-oriented approach are compromised. This compromise is more than an academic one. Consider a simple and very common example: adding a column to a table. Table A shows the steps necessary using both stateful and stateless objects.

Table A: Changes required to add a column to a table

Changes Necessary MTS/Stateless Objects Stateful Objects
Server Code Add argument to save/update method. Change read method. Add property let/get. Change, initialize, and save/update method.
Client Code Change call to save/update method to supply an additional parameter. Get new output from read method. Call property get/let.

MTS drawbacks

The major issue here isn't the amount of code; it's the nature of the code. You must consider two important points about the stateless approach. First, the order of the arguments matters. VB5 eases the pain of this problem if you use early binding, but you'll still end up with ugly, error-prone code when you're working with an object that has 10 or more data members.

Second, the stateless approach results in large blocks of code instead of small, self-contained blocks. The Save method will validate all of the arguments, so you don't have to deal with them in individual Property Let procedures. Of course, you can code private validation procedures, but the Save method still won't be pretty.

If MTS is a technology that your organization has committed to, situations will still arise when stateful objects are required. For such cases, you can use MTS for stateless objects and use conventional objects (on either the client or the server) for stateful ones. Another option would be to use MTS for both stateful and stateless objects. This choice keeps all the objects under the MTS roof, but compromises the transactional abilities of MTS.

Database access with MTS

Another key point to consider with MTS and stateless objects is that—depending on the nature of your application—database accesses will probably increase. This increase may offset some of the benefits of having a more efficient business-logic server.

For example, imagine a sale object, objSale, and a customer object, objCustomer. Every sale belongs to a customer. objSale may have to call objCustomer's IsPastDue procedure when adding or updating a sale. If objCustomer were stateful, we could just include a statement similar to the one following in objSale, and the information would already be in memory:

If Parent.IsPastDue Then GoTo _
Error_CustomerAccountPastDue

With stateless objects, we'd take one trip to the database to see if the customer's account was past due and another trip to add or update the sale. While we could handle this procedure on the client by keeping the data there, or on the database server through triggers, we'd be taking a big step back toward traditional client/server computing. So, let's look at another solution.

Reduce traffic

In order to get good performance from a business server on a remote machine, we must do something about multiple Property Gets and Lets. For example, a customer screen might have a customer object behind it with 20 properties (first_name, last_name, home_phone, etc.).

To populate the screen, we have to perform 20 Property Gets, at the cost of 20 network round trips. This scenario is comparable to having a traffic light at a busy intersection that turns green only long enough to allow one car to pass. Of course, there might also be times when only one field, maybe a balance field, needs to be refreshed, which would require only one network round trip—just as at midnight, maybe only one car is waiting for the light to turn green.

Data marshalling

The object user should be able to choose whether to batch the Property Gets and Lets or to execute them individually. In order to give the object user this flexibility in a consistent manner, we'll give every class that has more than one piece of persistent data two polymorphic methods, BatchPropertyGet and BatchPropertyLet. To avoid using index numbers and worrying about the order of parameters, we'll provide a client-side utility class and a server-side utility class to support these methods—we'll call them ClientProperties and ServerProperties.

Now, let's see how this process would work with our hypothetical customer class. Listing A shows what the client-side code might look like to either get or let properties in batch.

Listing A: Code to get or let properties in batch

Dim objCustomerClientBatch As New _
    ClientProperties
Call objCustomerClientBatch.Initialize _
    (objCustomer.BatchPropertyGet)

Debug.Print objCustomerClientBatch.OriginalProperty _
    ("first_name")
Debug.Print objCustomerClientBatch.OriginalProperty _
    ("last_name")
Debug.Print objCustomerClientBatch.OriginalProperty _
    ("home_phone")

objCustomerClientBatch.SetProperty _
    "first_name", "Steve"
objCustomerClientBatch.SetProperty _
    "last_name", "Francis"
objCustomerClientBatch.SetProperty _
    "home_phone", "555-555-5555"

objPerson.BatchPropertyLet _            objCustomerClientBatch.DirtyProperties
Set objCustomerClientBatch = Nothing

After dimensioning the ClientProperties object, we initialize it with a variant array returned from the BatchPropertyGet method on the server. After modifying some property values, we send the dirty properties to the BatchPropertyLet method by using the DirtyProperties method of the ClientProperties object.

Listing B shows what BatchPropertyGet and BatchPropertyLet might look like for the customer class. For BatchPropertyLet, we won't access the private data members directly. Instead, we'll use customers' Property Let procedures to ensure data integrity.

Listing B: BatchPropertyGet and BatchPropertyLet examples for customer class

Public Property Get BatchPropertyGet() _
    As Variant
Dim objServerBatch As New _
    ServerProperties

objServerBatch.AddProperty _
    "first_name", strFirstName
objServerBatch.AddProperty _
    "last_name", strLastName
objServerBatch.AddProperty _
    "home_phone", strHomePhone
objServerBatch.AddProperty _
    "work_phone", strWorkPhone
objServerBatch.AddProperty _
    "email", strEmail
   
BatchPropertyGet = _
    objServerBatch.OriginalProperties

Set objServerBatch = Nothing
End Property

Public Sub BatchPropertyLet(ByVal _
    ClientBatchProperties As Variant)
Dim objServerBatch As New _
    ServerProperties

objServerBatch.DirtyProperties = _
    ClientBatchProperties

If Not _
    IsEmpty(objServerBatch.DirtyProperty _
    ("first_name")) Then first_name = _
    objServerBatch.DirtyProperty _
    ("first_name")
If Not _
    IsEmpty(objServerBatch.DirtyProperty _
    ("last_name")) Then _
    last_name =
    objServerBatch.DirtyProperty _
    ("last_name")
If Not _
    IsEmpty(objServerBatch.DirtyProperty _
    ("home_phone")) Then _
    home_phone=
    objServerBatch.DirtyProperty _
    ("home_phone")
If Not _
    IsEmpty(objServerBatch.DirtyProperty _
    ("work_phone")) Then _
    work_phone =
    objServerBatch.DirtyProperty _
    ("work_phone")
If Not _
    IsEmpty(objServerBatch.DirtyProperty _
    ("email")) Then _
    email = _
    objServerBatch.DirtyProperty("email")
   
Set objServerBatch = Nothing
Save
End Sub

Listing C shows the code for the entire ServerProperties class. Note that the code for the ClientProperties class is very similar, with a reverse implementation.

Listing C: ServerProperties class

' Returned by GetBatchProperties,
' read only
Private aOriginalProperties() As Variant        

' Uncommitted property writes
Private aDirtyProperties As Variant                      

' Number of dirty properties
Private lngNextIndex As Long                         

Private Const NameColumn = 0                                     
Private Const ValueColumn = 1                                    
Private Const NumColumns = 1                                     

' Add a property to the internal array.
' If it already exists, replace the value.
Public Sub AddProperty(ByVal _
    PropertyName As String, ByVal _
    PropertyValue As Variant)

Dim lngPosition As Long

If lngNextIndex = 0 Then ReDim _
    Preserve aOriginalProperties(NumColumns, _
    lngNextIndex)

lngPosition = _
    PropertyExists(PropertyName)
If lngPosition >= 0 Then
    aOriginalProperties(ValueColumn, _
    lngPosition) = PropertyValue
Else
    ReDim Preserve _
        aOriginalProperties(NumColumns, _
        lngNextIndex)
    aOriginalProperties(NameColumn, _
        lngNextIndex) = PropertyName
    aOriginalProperties(ValueColumn, _
        lngNextIndex) = PropertyValue
    lngNextIndex = lngNextIndex + 1
End If

End Sub

' For large numbers of properties, it may
' be judicious to consider something
' other than a sequential search.
Private Function PropertyExists(ByVal _
    PropertyName As String) As Long

Dim i As Long
For i = LBound(aOriginalProperties, 2) _
    To UBound(aOriginalProperties, 2)
    If LCase(aOriginalProperties _
        (NameColumn, i)) = _
          LCase(PropertyName) Then
        PropertyExists = i
        Exit Function
    End If
Next i

PropertyExists = -1

End Function
Public Property Get OriginalProperties() _
    As Variant

' Return entire array of original values.
OriginalProperties = _
    aOriginalProperties()

End Property

Public Property Let DirtyProperties(ByVal _
    DirtyProperties As Variant)
    aDirtyProperties = DirtyProperties   
End Property

Private Sub Class_Initialize()
    lngNextIndex = 0
End Sub

Public Property Get DirtyProperty(ByVal _
    PropertyName As String) As Variant  
    DirtyProperty = _
        SearchArrayReturnValue _
            (PropertyName, _
            aDirtyProperties)
End Property

' Will return empty or variant.
Private Function SearchArrayReturnValue _
    (ByVal PropertyName, ByRef _
    SearchArray As Variant) As Variant

Dim i As Long
For i = LBound(SearchArray, 2) To _
    UBound(SearchArray, 2)
    If LCase(SearchArray(NameColumn, i)) _
        = LCase(PropertyName) Then
        SearchArrayReturnValue = _
            SearchArray(ValueColumn, i)
        Exit Function
    End If
Next i

SearchArrayReturnValue = Empty

End Function

Although this solution does compromise the purity of an object model, it's a scalable solution because the changes are predictable and systematic. Furthermore, the object user still has a choice about whether or not to batch the calls.

Planning your approach

A DCOM implementation requires more planning than if you're going to run your business-logic layer in-process. Batching property procedures is a good start toward a real plan.

Another key area to consider in the planning stages is resultset handling. ADO's recordset object supports client-batch functionality and is ideally suited for DCOM. You can also use RDO methods, such as GetRows and GetClipString, to roll your own data access classes that broker variant arrays and speed tedious control population code. VB5 has a broad enough tool set that many creative solutions are possible.

If you're planning future releases, scalability, updates, and enhancements, it won't take long to recoup the time you've invested in an object-based approach. Release 1.0 might take 25 percent (or more) longer to develop, but the result should be a more flexible, scalable, and maintainable application.

Before you abandon a traditional object-oriented approach and compromise some of these benefits, it may be worthwhile to see if there's some way to reap most of the benefits of object-oriented design and the benefits of DCOM as well. The utility classes we've detailed in this article should give you a good head start.

Steve Francis is the founder of Net Set, a development company specializing in Internet business solutions. He's a Microsoft Certified Solutions Developer and provides consulting services to Fortune 500 companies focusing on n-tier Microsoft solutions. You can reach Steve by E-mail at stevendf@dfw.net.

Copyright © 1998, ZD Inc. All rights reserved. ZD Journals and the ZD Journals logo are trademarks of ZD Inc. Reproduction in whole or in part in any form or medium without express written permission of ZD Inc. is prohibited. All other product names and logos are trademarks or registered trademarks of their respective owners.