- 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-2: Use Custom Interfaces
COM is more than just a technology for building softwareit is also a philosophy for building systems that evolve more easily over time. The designers of COM recognized that excessive coupling hinders evolution, and thus sought a mechanism that minimized the coupling between components. For example, VB clients typically reference a class directly:
Dim rEmp As CEmployee '** class-based reference = default interface
As we know from rule 2-1, this declaration implicitly couples the client code to the default interface of class CEmployee. Because an interface represents a binding contract between client and class, this coupling prevents the class from evolving. But what if you really need to make an interface change (e.g., to extend a class's functionality or to repair a design oversight)?
The COM solution is to embrace explicitly interfaces in both the clients and the classesan approach known as interface-based programming. Instead of presenting a single default interface, classes now publicize one or more custom interfaces. Clients then decide which custom interface they need, and couple to this interface much like before. The key difference, however, is that classes are free to introduce new custom interfaces over time. This allows the class to evolve and to serve new clients, yet remain backward compatible with existing clients. Interface-based programming is thus a design technique in which interfaces serve as the layer of abstraction between clients and classes. As shown in Figure 2.2, this minimizes coupling on the class itself.
Figure 2.2 Two views of an interface
How do you define a custom interface? Like classes, custom interfaces are created in VB using class modules. Unlike classes, they contain no implementation because a custom interface is simply a set of method signatures. For example, here's the default interface of CEmployee (from rule 2-1) rewritten as a custom interface named IEmployee:
'** class module IEmployee Option Explicit Public Property Get Name() As String End Sub Public Property Get Salary() As Currency End Sub Public Sub ReadFromDB() End Sub Public Sub IssuePaycheck() End Sub
Note the absence of implementation details (i.e., private members and code). A custom interface thus represents an abstract class, which is conveyed in VB by setting the class's Instancing property to PublicNotCreatable. This also prevents clients from mistakenly trying to instantiate your interfaces at run-time.
Once defined, custom interfaces must be implemented in one or more class modules. For example, here is the class CConsultant that implements our custom interface IEmployee:
'** class module CConsultant Option Explicit Implements IEmployee Private sName As String Private cSalary As Currency Private Property Get IEmployee_Name() As String IEmployee_Name = sName End Sub Private Property Get IEmployee_Salary() As Currency IEmployee_Salary = cSalary End Sub Private Sub IEmployee_ReadFromDB() ... '** read from a database into private members End Sub Private Sub IEmployee_IssuePaycheck() ... '** issue employee's paycheck End Sub
Observe that every member in the class is labeled private! Clients thus cannot couple to CConsultant in any way, allowing it to evolve freely. Compatibility is maintained by continuing to implement IEmployee.
In general, clients now have a choice when accessing a class: to use its default interface or to use any one of the custom interfaces it implements. This choice is expressed by declaring your reference variables of the appropriate interface. For example, here we are accessing a CConsultant object through the IEmployee interface:
Dim rEmp As IEmployee '** reference to custom interface Set rEmp = New CConsultant '** class that implements this interface rEmp.ReadFromDB txtName.Text = rEmp.Name txtSalary.Text = Format(rEmp.Salary, "currency")
This situation is depicted in Figure 2.3. Note that the CConsultant object publicizes two interfaces: a default and IEmployee. VB classes always define a default interface, enabling clients to use class-based references:
Dim rEmp2 As CConsultant '** class-based reference = default interface Set rEmp2 = ...
Figure 2.3 Referencing an object through a custom interface
This is true regardless of whether the interface is empty, which it is in the case of CConsultant because the class contains no public members. The variable rEmp2 is thus useless, because there are no properties or methods to access.
Now that we can define, implement, and use custom interfaces, you may be wondering: How exactly does all this help me evolve my system more easily? Whenever you need to change a private implementation detail, merely recompile and redeploy the component (be sure to read rule 2-5 before recompiling COM components in VB). And when you need to make an interface change, simply introduce a new custom interface. In other words, suppose you want to evolve the CConsultant class by applying some bug fixes as well as by making a few interface changes. You would define a new interface, IEmployee2, implement it within CConsultant, apply the other bug fixes, recompile, and redeploy.
When existing clients come in contact with instances of the revised class, the result is shown in Figure 2.4 (notice the third lollipop).
Figure 2.4 An existing client referencing a new version of class CConsultant
By introducing new interfaces, classes evolve to support new clients while remaining compatible with existing ones. Note that you have two choices when defining a new interface: It is completely self-contained or it works in conjunction with other interfaces. For example, suppose the motivation for IEmployee2 is to add parameters to the method ReadFromDB, and also to add a method for issuing a bonus. In the first approach, you redefine the entire interface:
'** class module IEmployee2 (self-contained) Option Explicit Public Property Get Name() As String '** unchanged End Sub Public Property Get Salary() As Currency '** unchanged End Sub Public Sub ReadFromDB(rsCurRecord As ADODB.Recordset) End Sub Public Sub IssuePaycheck() '** unchanged End Sub Public Sub IssueBonus(cAmount As Currency) End Sub
And then your classes implement both. For example, here's the start of the revised CConsultant class:
'** class module CConsultant (version 2) Option Explicit Implements IEmployee Implements IEmployee2 . . .
Although the class contains some redundant entry points (Name, Salary, and IssuePaycheck are identical in both interfaces), the advantage is that clients need to reference only one interfaceeither IEmployee or IEmployee2. The alternative approach is to factor your interfaces, such that each new interface includes only the changes and the additions. In this case, IEmployee2 would contain just two method signatures:
'** class module IEmployee2 (factored) Option Explicit Public Sub ReadFromDB(rsCurRecord As ADODB.Recordset) End Sub Public Sub IssueBonus(cAmount As Currency) End Sub
This eliminates redundancy in the class, but requires more sophisticated programming in the client. For example, here's the revised client code for reading an employee from a database and displaying their name and salary:
Dim rEmp As IEmployee '** one reference var per interface Dim rEmp2 As IEmployee2 Set rEmp = New CConsultant '** create object, access using IEmp Set rEmp2 = rEmp '** access same object using IEmp2 rEmp2.ReadFromDB ... '** read from DB/RS using IEmp2 txtName.Text = rEmp.Name '** access properties using IEmp txtSalary.Text = Format(rEmp.Salary, "currency")
This is depicted in Figure 2.5. Note that both variables reference the same object, albeit through different interfaces.
Figure 2.5 Each interface requires its own reference variable in the client
As your system evolves, different versions of clients and classes may come in contact with one another. For example, it's very common for classes to gain functionality over time, and thus for a single client to interact with numerous iterations of a class. This implies the need for a mechanism by which a compiled client, already deployed in production, can determine what functionality an object provides; i.e., what interfaces it currently implements. Such a mechanism, based on run-time type information (RTTI), is provided by every COM object and is accessed using VB's TypeOf function.
Suppose our system contains a number of different employee classes: CConsultant, CTechnical, CAdministrative, and so forth. All such classes implement IEmployee, but currently only a few have been revised to implement IEmployee2. Now, suppose the task at hand is to send out a bonus to every employee who is not a consultant. Assuming the employee objects are stored in a collection, we can iterate through the collection and simply check the interfaces published by each object:
Public Sub SendOutBonuses(colEmployees As Collection, _ cAmount As Currency) Dim rEmp As IEmployee, rEmp2 As IEmployee2 For Each rEmp in colEmployees If TypeOf rEmp Is CConsultant Then '** no bonus for you '** skip Else '** issue this employee a bonus... If TypeOf rEmp Is IEmployee2 Then '** use interface Set rEmp2 = rEmp rEmp2.IssueBonus cAmount Else '** issue bonus the old-fashioned way <human intervention is required> End If End If Next rEmp End Sub
Even though the default interface CConsultant is empty, we use it as a marker interface to identify consultants uniquely. Of the remaining employees (all of whom receive a bonus), we check for the IEmployee2 interface and apply the IssusBonus method if appropriate. Failing that, human intervention is required because the employee object does not provide an automatic mechanism. The beauty of TypeOf is that it is a run-time mechanism: The next time you execute it, it will respond True if the class has been revised to implement that interface. Thus, as more and more classes implement IEmployee2 over time, SendOutBonuses will demand less and less human intervention.
The previous discussion reveals another advantage of custom interfaces polymorphism (see rule 1-7 for a more precise definition). If you think of custom interfaces as reusable designs, then it makes perfect sense for different classes to implement the same interface. This leads to plug-compatible components, and a powerful, polymorphic style of programming in the client in which code is (1) reusable across different classes and (2) resilient to change as classes come and go. For example, consider once again a system with numerous employee classes that all implement IEmployee. As implied by Figure 2.6, our client-side code is compatible with any of these employee classes. Thus, if we need to pay everyone, this is easily done using the IssuePaycheck method implemented by each class:
Public Sub PayEveryone(colEmployees As Collection) Dim rEmp As IEmployee For Each rEmp in colEmployees rEmp.IssuePaycheck Next rEmp End Sub
Figure 2.6 Custom interfaces encourage polymorphism
In other words, IssuePaycheck is polymorphic and can be applied without concern for the underlying object type. Furthermore, if new employee classes are added to the system, as long as each class implements IEmployee, then the previous code will continue to function correctly without recompilation or modification. As you can imagine, given a large system with many employee types and varying payment policies, polymorphism becomes a very attractive design technique.
Lest we all run out and start redesigning our systems, note that custom interfaces come at a price. They do require more effort, because each interface is an additional entity that must be maintained. Custom interfaces also complicate the compatibility issue, in the sense that default interfaces are easily extended (as a result of built-in support from VB) whereas custom interfaces are immutable (see rule 2-5 for a detailed discussion of maintaining compatibility in VB). Finally, scripting clients such as Internet Explorer (IE), Active Server Pages (ASP), and Windows Scripting Host (WSH) cannot access custom interfaces directly. They are currently limited to a class's default interface. This last issue is problematic given the importance of scripting clients in relation to the Web. Thankfully, a number of workarounds exist (see rule 4-5) until compiled environments (such as ASP.NET) become available.
Generally, however, the benefits of custom interfaces far outweigh the costs. Custom interfaces force you to separate design from implementation, encouraging you to think more carefully about your designs. They facilitate design reuse as well as polymorphism. Of course, custom interfaces also serve to minimize coupling between clients and classes, allowing your classes to evolve more freely while maintaining compatibility. As a result, you'll be able to "field-replace" components as business rules change or bug fixes are applied, insert new components of like behavior without having to revisit client code, and define new behavior without disturbing existing clients. You should thus consider the use of custom interfaces in all your object-oriented systems, but especially large-scale ones in which design and coupling have a dramatic effect.
Custom interfaces are so important that COM is based entirely on interfaces. Clients cannot access COM objects any other way. Hence, COM programmers are interface-based programmers. In fact, there exists a language, the Interface Description Language (IDL), solely for describing interfaces. Often called "the true language of COM," IDL is what allows a COM object developed in programming environment X to be accessed from a client written in programming environment Y. Although typically hidden from VB programmers, there are definite advantages to using IDL explicitly to describe your custom interfaces. Read on; we discuss this further in the next rule.