This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
|
mindplus@microsoft.com |
Steve Zimmerman |
Four COM Interface Design Guidelines |
've spent much of my time over the last several months helping development teams around the country design applications that use COM effectively. I'm finding that although development tools are becoming more and more
sophisticated, they don't replace the need for a solid understanding of COM and the principles of effective object-oriented design. I appreciate new IDE features as much as the next developer, but no amount of wizard-generated code, native COM support, or IntelliSense® technology can keep uneducated developers from developing crummy interfaces. On the contrary, one side effect of bringing COM development to the masses may indeed be a proliferation of ill-conceived, poorly designed COM interfaces.
I should point out that nearly all of the developers I work with are sharp individuals who have an impressive understanding of the problem domain of the software they develop. Still, designing COM interfaces is uncharted territory for many developers, so I'd like to present four practical guidelines you might find helpful.
1. Model Small Entities with
Well-defined Behaviors
First, this interface has no methods, only properties, which means that the object does not perform any action. Rather than encapsulate changes to its data, it simply sits there waiting for an external client to change its state. The responsibility of handling overdraft charges, monthly finance charges, and interest income is left up to the client rather than being handled internally. Second, since every property has a propput accessor method, clients can make changes to the underlying data with little discretion. I can provide validation code that prevents clients from making illegal or unwarranted changes to the data, such as assigning a negative balance, but the lack of encapsulation introduces potential errors. For example, there's nothing to prevent a buggy client application from attempting to transfer money from one account to another without correctly updating the balances of both accounts. Finally, the interface is problematic because it attempts to represent several discrete objectsthe monetary balances of three different bank accountsthat should instead be modeled as a collection of polymorphic subobjects. As currently designed, the IBankAccounts interface is unprepared to efficiently handle customers with no checking account or customers with more than one line of credit. A better approach would be to redesign this interface as shown in Figure 3. Instead of attempting to comprehend all different types of bank accounts, the new IBankAccounts interface simply manages a dynamic collection of objects that expose an IBankAccount interface. Each different account type provides its own implementation of the IBankAccount interface, with methods that model well-defined actions: deposit, withdrawal, and transfer. Adding a new account type to the system does not require a change to either interface.
2. Choose Strongly Typed Parameters over VARIANTs and SAFEARRAYs
The implementation of the GetIndex interface method selects text from a database using a VARIANT indexwhich I've presumably chosen because applications developed using Visual Basic and Active Server Pages (ASP) will use this interface. A Visual Basic-based client application might erroneously use the interface in this way:
Can you find the error? The problem is that the use of the VARIANT data type allows clients to pass a text string as the index parameter for the GetInfo method, which it is not prepared to handle. If you've used VARIANTs, you might be tempted to point out that the way to solve this problem is to improve the robustness of the implementation code like so:
The new code is certainly an improvement over the previous implementation, but a much better approach would have been to simply change the index parameter type to a long. Visual Basic will automatically perform the necessary conversion from the VARIANT-encapsulated BSTR to long before calling the method.
Another reason some developers are attracted to weakly typed parameters is that it allows them to change the way an interface acts without actually changing its syntax. Here's an example:
The use of the VARIANT data type allows you to pass an ill-defined array of data (internally implemented using the SAFEARRAY construct) such that the interface doesn't have to change when you change the content of the information passed to the client via the GetProjectInfo method. As I've said before, this approach amounts to syntactic innuendo, leaving the client with an interface contract that means nothing. The client has no idea what information the GetProjectInfo method might pass back. This is a monumental no-no that brings me to my next recommendation.
3. Clearly Define and Don't
Change the Interface Semantics
Without additional semanticsdocumentation you must provide to facilitate the proper use of your classthere's simply not enough information to know what the GetInformation method will do. Here's a similar declaration using IDL that removes all doubt.
The [in] and [out] attributes allow the IDL definition to clearly describe the direction in which the data is travelling without requiring additional documentation. The [ref], [unique], and [ptr] attributes allow you to further specify the semantics of your interfaces by describing whether or not a pointer parameter can be null or a duplicate. The [size_is] and [length_is] attributes provide information that links an array parameter to another parameter that describes its length or size. Although these attributes are typically used for marshaling efficiency, they also provide additional information about the expectations of the interfaces they describe.
A benevolent side effect of using IDL is that it encourages every interface method to return an HRESULT. This takes some getting used to, but it's a good thing. It standardizes the approach to raising error conditions and accounts for the fact that errors can be introduced by the remoting layer in addition to the interface implementation code. Since the set of possible HRESULTs is part of the semantics for your interface, you should document the HRESULTs returned by your code. As a general rule, use existing error codes when possible and define custom HRESULTs when needed, but never use an existing error code in a way that would contradict its defined meaning. I recommend keeping successful return values separate from error codes. Although you can use HRESULTs to return successful (non-negative) results and other information, that practice is confusing and unnecessary. Use a separate [out] parameter instead. You don't need to describe error codes that occur out- side the scope of your code (such as those generated by the DCOM communication layer). But if you pass along an HRESULT from another method, you should document it. Unfortunately, that can be a nontrivial task, as illustrated by the following code:
It's not easy to document what error codes this method returns because it passes along HRESULTs from the CComObject<>::CreateInstance and IUnknown::QueryInterface methods in addition to E_FAIL.
4. Handle Versioning with Elegance
Conclusion
|