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 ServerMicrosoft 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. |
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 MTSAnother 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 trafficIn 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 marshallingThe 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 approachA 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.