- Rule 2-1: Think in Terms of Interfaces
- Rule 2-2: Use Custom Interfaces
- Rule 2-3: Define Custom Interfaces Separately, Preferably Using IDL
- Rule 2-4: Avoid the Limitations of Class-Based Events with Custom Callbacks
- Rule 2-5: Be Deliberate About Maintaining Compatibility
- Rule 2-6: Choose the Right COM Activation Technique
- Rule 2-7: Beware of Class_Terminate
- Rule 2-8: Model in Terms of Sessions Instead of Entities
- Rule 2-9: Avoid ActiveX EXEs Except for Simple, Small-Scale Needs
Rule 2-8: Model in Terms of Sessions Instead of Entities
When it comes to designing the object model for your system, you should consider whether to design your classes around sessions or entities. Entities represent a more traditional object-oriented approach, in which classes are based on real-world entities in your systemcustomers, orders, products, and so forth. In contrast, sessions represent the set of expected interactions between clients and your objects. Although session-based class designs may deviate from pure OODs, the motivation is performance over elegance. Session-based systems strive to streamline client interactions, which is particularly important when objects are out-of-process (e.g., in distributed applications).23
Obviously, the issue of design is no small matter. For example, consider a multi-tier system with business and data layers. How should the data access layer behave?24 Should it model each table as a class? If so, where do queries over multiple tables fit in? Perhaps there should be just one class per database. And what about the business layer? Are lots of smaller classes better than a few larger ones? How should they be grouped to take advantage of polymorphism in the client? Each of these questions may have different answers, based on system goals.
At a high level, most business systems are the same: They gather information from their users and submit this information for processing. The system is thus divided into at least two parts, the front-end user interface and the back-end processor. Each communication from the front-end to the back-end represents a unit of work and constitutes a round-trip. Session-based designs model user scenarios in an attempt to minimize round-trips. Traditional OODs often don't take into account the cost of a round-trip, yielding less than optimal performance.
For example, consider the traditional entity-based design of a CCustomer class. The class models the state and behavior of a customer in the system; in particular, allowing easy access to customer information:
'** class module: CCustomer (traditional OOD) Option Explicit Public Name As String Public StreetAddr As String Public City As String Public State As String Public Zip As String Public Property Get CreditLimit() As Currency '** return customer's credit limit for purchases End Property Public Sub PlaceOrder(ByVal lProductNum As Long, _ ByVal lQuantity As Long) '** code to place an order for this customer End Sub
Although straightforward to understand, consider what the client must do to change a customer's address:
Dim rCust As CCustomer Set rCust = ... With rCust .StreetAddr = <new street address> .City = <new city> .State = <new state> .Zip = <new zipcode> End With
The cost is four round-trips, one per datum. Likewise, placing an order for N different products requires N + 1 trips, one to check the customer's credit limit and another N to order each product.
A better approach, at least from the perspective of performance, is to redesign the CCustomer class based on the expected user scenarios: getting the customer's information, updating this information, and ordering products. This leads to the following session-based result:
'** class module: CCustomer (revised session-based design) Option Explicit Private Name As String '** no public access to data Private StreetAddr As String Private City As String Private State As String Private Zip As String Public Sub GetInfo(Optional ByRef sName As String, _ Optional ByRef sStreetAddr As String, _ Optional ByRef sCity As String, _ Optional ByRef sState As String, _ Optional ByRef sZip As String) sName = Name sStreetAddr = StreetAddr sCity = City sState = State sZip = Zip End Sub Public Sub Update(Optional ByVal sName As String = "?", _ Optional ByVal sStreetAddr As String = "?", _ Optional ByVal sCity As String = "?", _ Optional ByVal sState As String = "?", _ Optional ByVal sZip As String = "?") If sName <> "?" Then Name = sName If sStreetAddr <> "?" Then StreetAddr = sStreetAddr If sCity <> "?" Then City = sCity If sState <> "?" Then State = sState If sZip <> "?" Then Zip = sZip End Sub Public Sub PlaceOrder(laProducts() As Long) '** confirm that client passed a 2D array (products times quantities) Debug.Assert UBound(laProducts, 1) = _ UBound(laProducts, 2) '** code to check that credit limit is sufficient '** code to place entire order for this customer End Sub
First of all, notice there is no public access to customer data. All reads and writes must be done via methods. As a result, an address change now takes only one round-trip call to Update. Likewise, PlaceOrder is redesigned to accept an array of product numbers and quantities, allowing an entire order to be placed via one round-trip call. In short, the class contains one entry for each task that the user may need to perform. Although the class's interface is arguably more cumbersome for clients to use, the potential increase in performance is significant, especially across a network.
Session-based designs are usable at every level of a system. For example, in a standard multi-tier application, your business objects would model client sessions, whereas your data access objects model business object sessions. For the latter, your data access design may be as simple, and as efficient, as two methods: one to read and one to write:
'** class module: CDataAccess (minimal session-based design) Option Explicit Public Function ReadDB(sConnectionInfo As String, _ sSQL As String) As ADODB.Recordset '** code to open DB, build recordset, disconnect, and return it... End Function Public Function UpdateDB(sConnectionInfo As String, _ sSQL As String) '** code to open DB and update via SQL... End Function
Obviously, good design is the proper balance of usability, maintainability, extensibility, and performance.