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[2] 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.
Comments
Anonymous
January 23, 2004
The comment has been removedAnonymous
January 23, 2004
The comment has been removedAnonymous
January 23, 2004
Ugh, a Print property is definately bad. Similar to your question about "object.Property = object.Property", is what would happen if the user calls "object.Print = false"? Would it not print? Did the false value have some reserved meaning such as causing the screen to invalidate it's self? I guess I could change #2 to not only say UI, but also to include "physical side effects", such as printing.
The only possible reason for having a Print property would be to return an object called Print (the property name should always match the name of the object it returns) that had properties to set the printer tray, landscape/profile, and then a method called PrintOut that would actually perform the print operation. If that is what you would use it for, I would say it should be called Printer, a property name should be a noun, while a method could be a verb or noun depending on the context.
Hey, that is two more rules:
55. If a property is returning an object, the property name should be the same as the name of the object it returns.
56. A property should be a noun, while a method could be a noun or verb, depending on wether it modifies data or takes action on data, respectively.Anonymous
January 27, 2004
Somebody sent me mail asking for real world code demonstrating these principals, especially using VB.NET. Unfortunately I lost that mail.
I do not have any VB .NET samples using these guidelines. I wrote these guidelines for the Visual Studio object model, and (for the most part) these principals were applied to the design of the VS object model.Anonymous
June 06, 2004
Hi,
I am in process of designing an object model and i didnot find
any article about managing large collection,
My object model is persisted in a data base,and some collection
can have more than 10,000 enteries
also,how should an object model behave when deleting an object
while holding a reference to it?
Set Item=Items(1)
Items.Remove(1)
'What will happen here:
Item.FullName="Julia"
Item.Save
thanksAnonymous
June 08, 2004
We do not have any objects that would contain that many objects, so I have never investigated if that would have any impact on designing the object. You may not want to have a _NewEnum object, or carefully construct it so that it would be performant. We use ATL for most of our enumerators, and in the ATL model you construct an array, pack the opjects into that array, then walk over the objects. If you did that in this case, you would be using a lot of memory and it would not be very performant - you would be constructing one of these arrays for every call to _NewEnum.
As for the delete model, we use what we call the zombie model (I talked about this in an earlier post). What you do is, when a Remove/Delete method is called, you set a flag within the object indicating that the object is no longer an object that should be used (dead, but still up and referenced - a zombie). When you call methods on this object, we then return an error indicating that the call is not possible at that time, and a good case of where retrieving a property should throw an error message. In VS 2005 we have a new interface LifetimeInformation, and a property, HasBeenDeleted, which will allow the user to determine if the object they have is throwing errors because the object has been deleted or because of some other state. When an object is in the zombie state, the only method/property calls that we allow is to the DTE object, which always works unless we are shutting down. All other methods/properties fail.Anonymous
March 30, 2009
Hi, I was wondering if there is a guideline for handling 32- to 64-bit changes for parameters. For example, if a method takes a 32-bit long int parameter in a 32-bit build, should it also take the same size 'long' in a 64-bit build or could that be switched to a 64-bit long integer? ThanksAnonymous
June 01, 2009
PingBack from http://patiochairsite.info/story.php?id=1515Anonymous
June 01, 2009
PingBack from http://portablegreenhousesite.info/story.php?id=13541