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.


MIND

Extreme C++
mindplus@microsoft.com
Steve Zimmerman
Four COM Interface Design Guidelines
I
'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
      One of the most challenging architectural tasks in software development is decomposing a complex system into objects. Wise and judicious decomposition will promote syntactic elegance, decrease application complexity, encourage object reuse, and facilitate scalability. Unfortunately, despite the existence of numerous design methodologies, notations, and processes, there is no silver bullet method that guarantees you'll arrive at a perfect design. The best software architects are most often the ones who have the most hands-on experience. They've simply tried enough stuff to know what works and what doesn't.
      Still, there are several rules of thumb that make for better interfaces. When defining a COM interface, model behaviors and actions rather than structure. Consider the IBankAccounts interface (see Figure 1) that models the account balances of a given banking customer. Since I used Visual C++® to design this interface, I was able to take advantage of several cool productivity features, including the ATL Object Wizard and the handy

Figure 2: ClassView
Figure 2: ClassView
options offered by the context menu that appears when I right-click my interface in the Class View (see Figure 2). These razzle-dazzle IDE features made designing the interface a breeze, but three aspects of its design should immediately flag it as a poor interface.
      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 objects—the monetary balances of three different bank accounts—that 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
      In the previous Extreme C++ column (September 1998), I described the advantages of using strongly typed parameters in my discussion of COM collection classes, so I won't rehash that information here. But I want to make two more points about the use of strongly typed parameters that I didn't mention then. First, just because Visual Basic-based clients make wholesale use of VARIANTs doesn't mean you have to use that data type for your method parameters. Avoid the temptation.
      Say I define the following interface and implementation:

 interface IThingamaBob : IUnknown
 {
 HRESULT GetInfo([in] VARIANT vtVal, [out, retval] BSTR* pbstr);
 •••
 }
 
 STDMETHODIMP CMyThing::GetInfo(VARIANT vtID, BSTR* pbstr)
 {
     CCommand<CAccessor<CMyAccessor> > lookup;
     lookup.m_index = vtID.lVal;
     hr = articles.Open(m_session,
                        "SELECT text FROM mytable where index=?");
     *pbstr = CComBSTR(articles.m_text).Detach();
 }
The implementation of the GetIndex interface method selects text from a database using a VARIANT index—which 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:
 Dim webObj As IWebArticle
 Private Sub Validate_Click()
     Set webObj = New WEBTESTLib.WebArticle
     webObj.GetArticle (articleID.Text)
 End Sub
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:
 STDMETHODIMP CMyThing::GetInfo(VARIANT vtID, BSTR* pbstr)
 {
     if (vtID.vt != VT_I4)
     {
         HRESULT hr = VariantChangeType(&vtID, &vtID, 0, VT_I4);
         if (hResult != S_OK)
             return hResult;
     }
 
     CCommand<CAccessor<CMyAccessor> > lookup;
     lookup.m_index = vtID.lVal;
     hr = articles.Open(m_session,
                        "SELECT text FROM mytable where index=?");
     *pbstr = CComBSTR(articles.m_text).Detach();
 }
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:
 interface IProject : IDispatch
 {
     GetProjectInfo([out, retval] VARIANT* projInfo);
 };
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
      Most developers understand that the physical signature of an interface is an immutable contract between client and server. Once you publish an interface, you cannot alter its syntax in any way because those changes—whether adding or removing methods or altering method signatures—will result in a change of behavior that will cause the client to behave unexpectedly. In addition to its signature, there are additional characteristics of an interface that cannot be defined using a type library or a header file. These attributes, called semantics, must also remain essentially unchanged or the client will be adversely affected.
      Interface semantics describe the proper usage of an interface and define the way its methods will behave under various conditions. Semantics include descriptions of the behavior of each method, the conditions under which each method should be called, and the result codes (HRESULTs) returned by each method. The semantics of an interface do not dictate its underlying implementation, but do provide vital information about how the interface can be expected to behave.
      Since interface documentation is quite possibly the least-favorite and most-shirked duty of C++ developers worldwide, I'd like to point out several things you can do—in addition to using strongly typed parameters—that will help reduce the amount of semantics documentation you'll need.
      First, always define your interface using Interface Definition Language (IDL). For an IDL refresher course, refer to the article "Understanding Interface Definition Language: A Developer's Survival Guide" by Bill Hludzinski (MSJ, August 1998). Even though IDL is technically required only when you need the help of the MIDL compiler to generate type libraries or proxy-stub code, you should always use IDL when designing interfaces. IDL allows you to specify attributes not defined in C and C++ that affect the behavior of your interfaces. The [in] and [out] attributes provide information about the direction in which parameters are passed—into the method, out of the method, or both. Given the following C++ declaration, does GetInformation receive information, or is it responsible for copying data back out?

 class CMyClass
 {
 public:
 virtual void GetInformation(wchar_t* pInfo) = 0;
 •••
 };
Without additional semantics—documentation you must provide to facilitate the proper use of your class—there's simply not enough information to know what the GetInformation method will do. Here's a similar declaration using IDL that removes all doubt.
 interface IMyInterface : IUnknown
 {
     HRESULT GetInformation([in] BSTR bstrInfo);
 }
      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:
 HRESULT CAnimals::Add(BSTR bstrName, IDispatch** ppItem)
 {
     CComObject<CAnimal>* pObj;
     HRESULT hr = CComObject<CAnimal>::CreateInstance(&pObj);
     if (!SUCCEEDED(hr))
         return hr;
 
     pObj->SetName(bstrName);
 
     IAnimal* pIFace = NULL;
     hr = pObj->QueryInterface(IID_IAnimal, (LPVOID*)&pIFace);
     if (FAILED(hr) || pIFace == NULL)
         return E_FAIL;
 
     m_itemList.push_back(pIFace);
     return pIFace->QueryInterface(IID_IDispatch, (void**)ppItem);
 }
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
      Once you've published an interface, you should not make any modifications that would change its syntax or significantly alter its semantics. So how do you allow your components to progress over time or increase in sophistication? You expose another interface. You can either inherit from the existing interface or develop a completely new one, as long as the old interface remains available to legacy clients. You can do this in one of two ways: support both interfaces in a component that you install on top of the old one, or support only the new interface in a component that you install to a separate location, leaving the old component alone. In Figure 4, I've outlined the advantages, disadvantages, and requirements of each approach.

Conclusion
      I've discussed four programming guidelines that lead to better interface design: model small entities with well-defined behaviors; use strongly typed parameters; define the interface semantics; and handle versioning with elegance. These guidelines by no means represent a conclusive list of the rules of thumb for effective COM development, but they'll give you a good start.
      The last piece of insight I'd like to offer is this: when one of the world's leading authorities on object-oriented design was asked his approach to architecting effective solutions, he replied, "I try things." Practice makes perfect, I guess. Happy coding!

From the November 1998 issue of Microsoft Interactive Developer.