Confounded by implements and interfaces? In this article, R.L. Parker not only defines them, but also shows how you can use VB5's new interface inheritance to improve the performance, reliability, and maintainability of your programs.
Here's a situation you often run into when developing business applications: Payments are accepted for goods or services. A single service might be paid for with one or more payments, and a variety of payment methods are accepted. The payment information must be recorded and later displayed. To illustrate the points in this article, I'll concentrate on displaying the payment information. There's some common information that must be displayed for each payment, no matter what its type is; there's other information that's unique to each method. A logical model that illustrates the situation is shown in Figure 1. In this example, payment type and payment amount should be displayed for every payment. Additionally, for check payments, I need to display the check number; and for credit card payments, I need to include the credit card type and number, the expiration date, and the name on the card.
Figure 1: A logical model that supports the storage and retrieval of payment information
In object-oriented parlance, I want the payment types to be polymorphic with respect to the request to display payment information. What does that mean? It means that each payment type must be able to display its information, but the payment type itself determines the appropriate information to display.
In this article, I'll discuss a feature of VB5 that can be used to improve the speed, the reliability, and the maintainability of code written to solve problems like this one.
The problemIn VB4, I can create separate classes to represent each of the payment types, as in Listing 1 and Listing 2. I can make these classes polymorphic with respect to each other by giving them common methods and properties such as "Amount" and "OtherDetails."
Listing 1. The VB4 CheckPayment class.
Private mcAmount As Currency
Private msDateRecd As String
Private msCheckNumber As String
Public Function OtherDetails() As String
OtherDetails = "Check Number = " & msCheckNumber
End Function
Public Property Let Amount(cAmount As Currency)
mcAmount = cAmount
End Property
Public Property Get Amount() As Currency
Amount = mcAmount
End Property
Public Property Get CheckNumber() As String
CheckNumber = msCheckNumber
End Property
Public Property Let CheckNumber(sCheckNumber As String)
msCheckNumber = sCheckNumber
End Property
Public Property Get DateReceived() As String
DateReceived = msDateRecd
End Property
Public Property Let DateReceived(sDate As String)
msDateRecd = sDate
End Property
Listing 2. The VB4 CreditCardPayment class.
Private mcAmount As Currency
Private msDateRecd As String
Private mnCardType As Integer
Private msNumber As String
Private msExpires As String
Private msName As String
Public Property Let Amount(cAmount As Currency)
mcAmount = cAmount
End Property
Public Property Get Amount() As Currency
Amount = mcAmount
End Property
Public Function OtherDetails() As String
Dim sTemp As String
sTemp = "Credit Card Type: " & _
Choose(mnCardType, "American Express", "MasterCard",_
"Visa")
sTemp = sTemp & vbCrLf
sTemp = sTemp & "Credit Card Number: " & msNumber
sTemp = sTemp & vbCrLf
sTemp = sTemp & "Expiration Date: " & Format$_
(msExpires, "mm/dd/yyyy")
sTemp = sTemp & vbCrLf
sTemp = sTemp & "Name on Card: " & msName
OtherDetails = sTemp
End Function
Public Property Get CCNumber() As String
CCNumber = msNumber
End Property
Public Property Let CCNumber(sNumber As String)
msNumber = sNumber
End Property
Public Property Get CCType() As Integer
CCType = mnCardType
End Property
Public Property Let CCType(nType As Integer)
mnCardType = nType
End Property
Public Property Get CCExpires() As String
CCExpires = msExpires
End Property
Public Property Let CCExpires(sDate As String)
msExpires = sDate
End Property
Public Property Get CCName() As String
CCName = msName
End Property
Public Property Let CCName(sName As String)
msName = sName
End Property
Public Property Get DateReceived() As String
DateReceived = msDateRecd
End Property
Public Property Let DateReceived(sDate As String)
msDateRecd = sDate
End Property
Now, given a collection of instances of these classes, I can iterate through the collection and display information about each instance. Notice especially the following code from the ShowPayment procedure. (See complete source code for Listing 3 in the Subscriber Downloads area at www.pinpub.com/vbd).
Listing 3. Fragment of code associated with the VB4 form.
Dim oPayment As Object
' more code here
Set oPayment = moPayments(mnPayment)
lblData(LBLDATA_PAYMENT_AMOUNT) = oPayment.Amount
lblData(LBLDATA_OTHER_DETAILS) = oPayment._
OtherDetails
This code works, as you can see in Figure 2 and Figure 3. So what's the problem? Actually, there are several. The first two problems are related to this line:
Dim oPayment As Object
Since I don't know what kind of payment I might need to display at runtime, I must use the generic Object type, because it can hold an instance of either payment class. But this declaration forces VB to use slow, late binding. Furthermore, since oPayment is a generic Object, I could (accidentally) assign any kind of object to oPayment at runtime. Of course, when I invoke the Amount and OtherDetails methods on an object that doesn't support those methods, I'll get a runtime error.
Figure 2: Payment 1 displayed by an instance of CheckPayment
Figure 3: Payment 2 displayed by an instance of CreditCardPayment
Another problem is that the compiler can't help me enforce the rule that all payment classes must implement the Amount and OtherDetails properties. Suppose I add a third payment class to the application. If I forget to declare the Amount properties or the OtherDetails method, I'll get a runtime error when I reference the missing property or method in an instance of the new class. (Do you share my sense of "been there, done that"?)
What I'd really like to have in situations like this is an abstract base class that would define the common methods and properties that all subclasses are required to implement. I'd also like a technique that would allow me to implement the abstract base class in any number of concrete child subclasses. Such a tool would solve all the problems I've mentioned: When time to compile, the compiler would remind me if I'd forgotten to declare an inherited property or method in the child class. At runtime, I could assign arbitrary instances of any of the concrete classes to a variable of the base class, reaping the benefits of polymorphism without paying the price of late binding.
The solutionVB5 has introduced interface inheritance to the VB programmer's toolkit. First, I'll explain the meaning of interface inheritance; then, I'll demonstrate how it can be used to improve the payments display code.
In VB5, at runtime, a class instance is a Component Object Model (COM) object. COM objects implement one or more COM interfaces (a COM interface being a related collection of functions). All COM objects must implement the IUnknown interface, but they're also free to implement as many other interfaces as you'd like. Every VB class implements IUnknown automatically, in addition to another interface that's defined by the set of public members and properties that are declared in the class. A VB class can also implement the interface defined by a second VB class-this is known as interface inheritance.
By defining a VB class-I'll call it class ABC-whose methods have no implementation and whose interface is implemented by other VB classes, you'll get something very much like an abstract base class. That is, ABC defines what any instance in this class hierarchy can do, but not how it will do it. The concrete classes that implement ABC, on the other hand, determine how to implement each property and method in the interface. ABC isn't really an abstract base class, because instances of it can be declared and used. You'll see shortly that this fact can be used to your advantage.
As an example, I'll return to the payments display problem. Here's the declaration of an abstract base class I'll call Payment; it defines the operations that every payment class must support:
Public Property Get Amount() As Currency
End Property
Public Property Let Amount(cAmount As Currency)
End Property
Public Function OtherDetails() As String
End Function
That's it-the whole thing! The Payment class has methods, but no implementation. Looks kind of strange, doesn't it? Now look at the new VB5 version of the CheckPayment class (see Listing 4) and CreditCardPayment class (see Listing 5). Pay special attention to this line:
Implements Payment
Listing 4. The VB5 CheckPayment class (Version 1).
Implements Payment
Private mcAmount
Private msCheckNumber
Private Function Payment_OtherDetails() As String
Payment_OtherDetails = "Check Number = " & _
msCheckNumber
End Function
Private Property Let Payment_Amount(cAmount As Currency)
mcAmount = cAmount
End Property
Private Property Get Payment_Amount() As Currency
Payment_Amount = mcAmount
End Property
Public Property Get CheckNumber() As String
CheckNumber = msCheckNumber
End Property
Public Property Let CheckNumber(sCheckNumber As String)
msCheckNumber = sCheckNumber
End Property
Listing 5. Fragment from the VB5 CreditCardPayment class (Version 1).
Implements Payment
Public Enum CreditCardType
ccAmericanExpress
ccMasterCard
ccVisa
End Enum
Private mcAmount As Currency
Private msNumber As String
Private mnType As CreditCardType
Private msExpires As String
' See Subscriber Downloads for complete listing
The Implements keyword, new in VB5, makes interface inheritance possible. It says that the CheckPayment class is not only implementing its own interface (by its public methods and properties), but it's also implementing the interface it "inherits" from the Payment class. Note that the methods inherited from the Payment interface are not declared public in the CheckPayment interface.
So how do you use one of these multiple interfaces? Look at the VB5 form code in Listing 6. You'll notice that the ShowPayment procedure has been changed. The variable oPayment is declared as Payment instead of as Object. This improves the code in two ways: It enables early binding, and it prevents us from accidentally assigning an instance of any other type to the oPayment variable.
Listing 6. The ShowPayment procedure in the VB5 Form code (Version 1).
Private Sub ShowPayment()
Dim oPayment As Payment
Dim nCount As Long
nCount = moPayments.Count
If nCount >= 1 And mnPayment <= nCount Then
Set oPayment = moPayments(mnPayment)
If moPayments.Count > 0 Then
lblData(LBLDATA_PAYMENT_AMOUNT) = _
oPayment.Amount
lblData(LBLDATA_OTHER_DETAILS) = _
oPayment.OtherDetails
End If
End If
fra(1).Caption = "Payment # " & CStr(mnPayment)
End Sub
Another point to note about these multiple-interface classes is that you can only invoke the methods defined in an interface through a reference to that interface. To see what I mean, look at the code in GetPayments, which builds the payments data that will be displayed. When I want to assign values to properties of the CheckPayment instance, I invoke the properties through the oCheck variable that is declared as CheckPayment. But when I want to assign a value to the Amount property, I must first assign the oCheck reference to a variable that I declared as Payment. This makes sense when you recall that Amount is not a public property of the CheckPayment class:
Dim oPayment As Payment
Dim oCheck As CheckPayment
Set oCheck = New CheckPayment
oCheck.CheckNumber = "1001"
Set oPayment = oCheck
oPayment.Amount = 100
Multiple inheritance
By now, it might have occurred to you that if a class can implement one other class's interface, perhaps it can implement more than one other class's interface. Indeed it can, which brings us to VB5's version of multiple inheritance.
Returning to the payments display, suppose there's a requirement to log a transaction each time a payment is viewed. Here's the declaration for the abstract base class that defines the desired functionality-I'll call it EventLog:
Public Sub LogEvent(sEvent As String)
End Sub
Now, if you look at the revised CheckPayment class (listing07.wri in the Subscriber Downloads) and CreditCardPayment class (listing08.wri in the Subscriber Downloads), you'll see a second Implements line:
Implements EventLog
The implementation of the EventLog interface in the CheckPayment class looks like this:
Private Sub EventLog_LogEvent(sEvent As String)
MsgBox Date & " " & Time & ": " & sEvent
End Sub
The ShowPayment procedure (see Listing 7) has been modified to take advantage of the new functionality. As before, to invoke the method from the new interface, I must assign the instance to a variable of that type:
Dim oEventLog As EventLog
Dim oPayment As Payment
Set oEventLog = oPayment
oEventLog.LogEvent "Viewed Payment #" & CStr(mnPayment)
Listing 7. The ShowPayment portion of the VB5 form code that uses the multiple inheritance classes.
Private Sub ShowPayment()
Dim oEventLog As EventLog
Dim oPayment As Payment
Dim nCount As Long
nCount = moPayments.Count
If nCount >= 1 And mnPayment <= nCount Then
Set oPayment = moPayments(mnPayment)
If moPayments.Count > 0 Then
lblData(LBLDATA_PAYMENT_AMOUNT) = oPayment._
Amount
lblData(LBLDATA_OTHER_DETAILS) = oPayment._
OtherDetails
End If
End If
fra(1).Caption = "Payment # " & CStr(mnPayment)
Set oEventLog = oPayment
oEventLog.LogEvent "Viewed Payment #" & Cstr_
(mnPayment)
End Sub
It works! See Figure 4.
Figure 4: The EvenLog interface in action
DelegationA child class must implement all the public methods and properties in the interface it inherits. But it might make sense for the base class to provide a default implementation. Then child classes would have several options: They could use the default implementation only, they could use the default implementation and add to it, or they could ignore the default implementation altogether and provide a custom implementation instead.
If you look closely at listing07.wri and listing08.wri in INHERIT.exe in the Subscriber Downloads, you might notice that they have identical implementations for the EventLog method. As an alternative, I've modified the EventLog class to provide an implementation of the LogEvent method. Here's how it looks:
Public Sub LogEvent(sEvent As String)
MsgBox Date & " " & Time & ": " & sEvent
End Sub
To use the default implementation, I'll need an instance of the base class in the subclass, which I've declared as a module-level variable:
Private moEventlog As EventLog
Now the child class implementation can simply pass the call on to the default implementation in the base class:
Private Sub EventLog_LogEvent(sEvent As String)
moEventlog.LogEvent sEvent
End Sub
Conclusion
Interface inheritance is an incredibly useful tool for the VB programmer who understands it and can apply it in appropriate situations. In those situations, it provides improved application robustness, performance, and maintainability.
Download sample code here.R.L. Parker is a senior programmer at DBBasics, Inc. in Raleigh, NC, where he specializes in custom development of mission-critical database applications. rlp@dbbasics.com.