The 54 commandments of COM object model design.
There are a plethora of books, tutorials, whitepapers, etc. on the market describing how to use an existing object model. But I have never seen any real description of how to design a COM object model (the .NET framework has design guidelines, but I have not seen one for COM), even though it is a question that I occasionally see on newsgroups, mailing lists, etc. I thought I would share the list of rules that we follow when developing the Visual Studio COM object model. There are a few blemishes in our object model, mostly caused by legacy code in our code base, where we do not follow these rules, but we try to follow them as close as possible. These rules may be applicable to your COM object model (with some changes, such as your root object probably will not be called DTE), and some are applicable to .NET development (translate HRESULTs to exceptions). I encourage you to make a list of rules such as this when you design an object model, it makes defining the objects much easier and consistent.
1. No UI is shown to the user when calling a method except in the case where the method name implies that UI will be shown. (Examples of this exception are ExecuteCommand and PromptToSave).
2. UI is never shown when calling a property (BUG: ItemOperations::PromptToSave)
3. Retrieving the value of a property should not cause side-effects to the object model. For example, calling the get_FullName property should not cause the file to be saved to disk.
4. All objects use only OLE Automation types.
5. When methods/properties take a numerical index, that index must be 1 based (first item is 1, not 0).
6. Item on collections objects is a method, not a property.
7. _NewEnum on collections objects is a method, not a property.
8. Items that are accessed from a collection have a property called Collection that returns the collection that generated the object, except in the case of a ‘2’ interface.
9. All objects that are not accessed from a collection have a property called Parent that returns the object that created the item, except in the case of a ‘2’ interface.
10. All objects have a DTE property that returns the DTE object, except in the case of a ‘2’ interface.
11. All collections end in an ‘s’, or the plural form of the object it is a container of.
12. All collections have the properties/methods (in addition to Parent & DTE) Count, Item and _NewEnum.
13. All event objects are returned from the DTE.Events object.
14. A failure HRESULT should never be returned from a property. If a property returns an object and an object is not available to be returned, then the property must return S_OK and NULL. If a string should be returned but a string is not available, then the empty string (“”) should be returned. If the value is numeric, return 0, and if the value is an enumeration then a enumerated value such as None should be returned. The only time it is permissible to return an error code from a property is if the object has been zombied, and there is no such property value to return. (Undetermined scenario: what if the property takes an argument?)
15. If a method returns an object, and an object is not available to be returned because an index was supplied and that index does not exist, the HRESULT E_INVALIDARG is returned.
16. If a method returns an object, and an object is not available for reasons other than an indexing problem, the HRESULT E_FAIL is returned and the out parameter is set to null/NULL/Nothing.
17. A method should avoid taking an automation object as an argument; instead, the method should accept the argument which could be passed as the index to the collection’s Item method. This avoids the case where the user spoofs an object. An example where we violate this rule and why it is bad: Commands.AddNamedCommand. The user could implement the interface AddIn (either because they don’t understand the model or they are up to no good), and pass this interface as the first arg to AddNamedCommand. Internally, we could use this interface in ways the bad passed-in version does not expect, and we would fail (fail either with a bad HRESULT, or catastrophically).
18. If a pointer is passed to a method, and that pointer is invalid (because it is NULL, etc), then the HRESULT E_INVALIDARG is returned except in the case where the argument is returning data back to the caller, then the HRESULT returned is E_POINTER.
19. All enumerations have the form: [Product Abbreviation][EnumType]. For example, the enumeration for the state of a window (Normal, Maximized, and Minimized) is called vsWindowState.
20. The values in a enumeration have the form:[Product Abbreviation][Enum type abbreviation][Value]. For example, the name of the window minimized state is vsWindowStateMinimized.
21. All general objects have a Kind property that returns a GUID that uniquely identifies the object type (for example, there is a Kind on Window, Project, ProjectItem and ProjectItems)
22. If a general object can return one or more specific objects, then those objects are returned from an Object method that takes a string parameter, or an Object property that takes no arguments. The string argument is used if the object can supply more than one object type. It is suggested that even objects that have only one specific object take a string parameter (for compatibility in the future when multiple objects are available). The string should be case insensitive. An example of this is the Document.Object method, which can return a TextDocument (if the TextDocument parameter is passed).
23. Properties that return a complete path to a file on disk are called FullName.
24. Properties that return the modified state of an item (an item that has not yet been committed to disk) are called Saved.
25. All methods that set an item to the item with focus are called Activate. By convention, calling Activate on an item does not necessarily make its container active. For example, calling Activate on a tool box item does not make the tab that contains that item active. Similarly, making an item active does not make the tool box window active.
26. The name of a method that removes an item from a collection is Delete if that item is a UI element (example: ToolBoxTab)
27. The name of a method that removes an item from a collection is Remove or Delete. Use Remove if it is non-destructive, Delete if it is destructive. Example: Solution.Remove removes a project from the solution, but does not delete it from disk. Commands.Delete removes the item from the collection, and destroys any data associated with that command causing it to no longer be available to the user.
28. All Remove & Delete methods are on the specific item’s object, not its collection.
29. The method to add an item to a collection is called Add. It is located on the Collection that creating a new item will add it to.
30. Properties (not to be confused with the Properties object, these are properties with a lower case p) should not take arguments, they should be methods.
31. All collections are returned via properties except if the item takes an index, then the collection is returned with a method. These properties are named the same as the object they return.
32. Any item that creates a user interface element does not make that element visible. Rather, the element object is returned, and the Visible property on the element can be used to make it visible. All other methods/properties that affect UI is made immediately. Example: CreateToolWindow does not show the window, you must specifically call Visible.
33. The most meaningful visible name of an Object is a property, and it is given a dispid of DISPID_VALUE making it the default value (for example, the Name of an object is made the default).
34. All objects should contain a default method or property. If the object is a collection, the default method should be the Item method. If the object is a collection item, then the default property should return the name of the object.
35. If an object has a default property, you should be able to use that default property as the index in the collection Item method.
36. Once a type library for an automation object has been shipped, under no circumstances shall that type library or the PIA wrapper around that type library be modified. It is set in stone. That same exact type library and PIA will be shipped for all future versions of the product.
37. If new functionality is needed after shipping an object model, then any interface definitions are placed in a new idl file, type library, and PIA.
38. If new functionality is needed to add to an existing interface, then a new interface is created in a new type library, the name of the interface is the OldInterfaceNameX (where X is a consecutive number beginning with 2), and the new interface derives from the old interface.
39. If a method needs to be modified by the addition or removal of new arguments, rules 37, 38, and 39 are to be applied, the method name should be OldMethodNameX (where X is a consecutive number beginning with 2), and the new method should follow, as closely as possible, the same order of arguments as the old method. For example void Foo(int bar, int baz) becomes void Foo2(int bar, int baz, int blah).
40. If a new interface is added to an object (such as an OldInterfaceName2 interface), then the new interface is accessible by casting an instance of the object to the new interface.
41. Care should be used to make sure that the COM DISPIDs of InterfaceName and InterfaceName2 do not conflict.
42. If a new interface is added to an object, all methods and properties should return an instance of the old interface, not the new one. Example: Windows2.CreateToolWindow2 returns a Window object, not a Window2 object.
43. Except to get to an extended version of an object, casting an object to get to an automation interface should be avoided. Another exception to this rule is for service-level interfaces, such as IDispatch, ISupportErrorInfo, LifetimeInformation etc.
44. No interfaces begin with the letter I unless the name of what is being modeled begins with an I. For example, IncrementalSearch is OK, but IWindows is not. The exception to this is if the interface is to be implemented by the user. Example: IDTExtensibility2, IDTCommandTarget, etc.
45. If your type library defines an interface that the user must implement, then that interface begins with the letters IDT, as in IDTWizard, IDTExensibility2, IDTToolsOptionsPage.
46. If a ‘2’ interface is added, the methods / properties DTE, Item, Parent, _NewEnum, Collection or any other method or property that has a name the same as the ‘1’ interface is not added to the new interface. These methods / properties are inherited from the ‘1’ interface.
47. If an interface (called Interface’) has an .Object method / property, any Parent properties on the object returned (called Interface’’) should return the Interface’ object, not the Parent of Interface’.
48. No arguments of a method (except the return argument, i.e. those marked with the IDL attributes [out, retval]) are to use Hungarian notation. They should (again with the exception of [out, retval] arguments) use Pascal casing.
49. Property, method, interface, and coclass names should use Pascal casing.
50. Dispinterfaces should start with the name _disp, and use camel casing (eg: _dispSolutionEvents)
51. Interfaces which shadow coclasses should have the same name as the coclass, except prepended with the underscore character. Example, _DTE the interface, DTE the coclass.
52. All interfaces must at least have the IDL attributes odl, dual, oleautomation, object, uuid, and helpstring.
53. All methods and properties must use at least the IDL attributes id and helpstring.
54. All automation consumable interfaces must derive from either IDispatch, or another interface which derives from IDispatch.