The Component Object Model
The component object model is built around the notions of components(often called
coclasses), objects, and interfaces. These three different entities
are defined and related as follows:
- Loosely, as we have just been discussing, a coclass (named from component
object class) is a piece of binary code that implements some kind of
functionality. Coclasses can be distributed in DLLs, or in executable files. It
is possible for a single module (DLL or executable) to contain more than one
coclass.
- A COM object is an instance of a coclass that gets loaded into memory.
(By the same token, one might say that a coclass is a 'blueprint' for a COM object.)
It is not unreasonable (indeed, it is quite common) for more than one object of a given
coclass to be active at a time.
- COM interfaces are the means — the only means — by which
other components and other programs get access to the functionality of a COM
component. An interface is a set of definitions of logically-related methods
that will control one aspect of the component's operation. Each component can
have one or more interfaces.
Note: You should have a good understanding of the concepts behind terms like "class"
and "object" from your experience of object-oriented programming with C++. These concepts
are not altered by COM, but you should be careful not to assume that there is a
complete, one-to-one mapping that encapsulates all the secondary meanings of
these terms. A coclass can be defined in C++ using a single C++ class,
but it could also be defined with many C++ classes, or without any C++ classes
at all (COM classes can be created in plain old C, for example). Remember that
COM is a language-neutral protocol.
These definitions are accurate, but they probably haven't done a great deal to
straighten things out in your mind — for a start, they're written in terms of one another.
Let's start again with a closer look at the most important of the three: the COM
interface.
Depending on the context in which it's being
used, the word "interface" can have slightly different meanings. In the broadest
possible terms, a COM interface is a group of definitions of methods that are
usually related in the operations they perform. The methods in an interface can
be called by using a pointer to that interface, and doing so results in the
execution of the code in a COM object.
A "human" component, for example, might have interfaces called IMouth
and IHand. These interfaces would group different methods — for example,
IMouth might contain methods called Eat(),Talk(), and Kiss(),
and IHand might contain Write(), Point() and Scratch().
Choosing our words a little more carefully, we can say that an interface is an
abstraction. The definition of an interface includes the syntax of the methods
it contains (return types, parameter types and calling conventions), and the
semantics of how to use them. To see how the latter can be important, consider
that there are often restrictions placed upon implementers and users of an interface that
just can't be described in code. A requirement such as the need to call an 'Init()'
method on the interface before calling any other method needs to be clearly documented,
and forms the semantic part of the interface definition.
More carefully still, an interface actually
has a very specific structure: it is an array of pointers to the implementations
of the functions it contains — this is the binary standard that we mentioned
earlier. Because the implementation of each function in the interface is
accessed by a pointer in an array, the precise order of the items in that array
is an important part of the interface's definition. Pictorially, we have a
situation like this:
COM Interface
The array of function pointers associated with an interface is usually known
as a vtable because it has the same structure as that produced by most
compilers for the virtual function table of a C++ class.
Notice that the definition of an interface
does not include an implementation of
its methods. When a component says that it's going to implement an interface,
it's up to that component to do so in a way that is both appropriate to itself
and in accordance with the semantics defined for that interface. This separation
leads to more robust design: interfaces can be reused in different situations,
and a component that makes a particular interface available can be swapped with
another component that makes available (exposes) the same interface. As a client,
if you know that you need the functions of a particular interface, all you need to do
is find an object that implements it.
We began this chapter by talking about the desire to build reusable components,
but from the point of view of the user (usually called the client), the important
aspect is not what the component is, but what it can do. Because interfaces
are the only way of making a component do anything at all, we can say that the
functionality of a component is defined by the interfaces it exposes. For example,
if you want to say that a coclass is both a lawyer and a philanthropist (and if you don't
feel that's an oxymoron), you can do so by having it expose interfaces called (say)
'ILawyer' and 'IPhilanthropist'.
By convention, the names of all interfaces start with 'I'. For fun, you can give your
interfaces names like 'IAmTheWalrus', 'IRobot', 'ICLAVDIVS', or 'IDo', but you may find
that the joke soon wears thin.
The COM specification includes details of a number of standard interfaces
that Microsoft has defined. By implementing one of these interfaces, a component states
that it supports some kind of functionality, or that it will work in some given
situation. For example, a coclass that implements 'ISupportErrorInfo' is able to return
rich error information, while a component implementing 'IDataObject' is capable of allowing
data to be pasted or dropped into another application.
Interfaces As Contracts
As we've explained, COM enforces complete
encapsulation of the data and implementation of a component. You can only call
methods on the interfaces exposed by a component; you never get direct access to
its data. This fact is what makes interfaces so fundamental, and when we link it
to our earlier assertion about COM allowing easy customization and upgrading of
applications, we can reach a couple of important conclusions:
- An interface, once defined, must never change. Published interfaces are
immutable.
- Once a component has said that it exposes an interface, any future version of
that component should also support that interface, to avoid existing clients from
malfunctioning.
The interfaces that a component exposes represent a 'contract' between the component
and its clients. A consequence of the second of these points when taken in the context
of the first is that changes made to the contract in order to 'update' a component
will surely break any existing clients, and so any revisions must be made with care.
We'll return to this subject in the next chapter.