As you read through this book, it is important to remember the difference between an object and an interface: you can implement an object however you want, to do whatever you want; and when you want to share its functionality and content with other components, you can provide as many interfaces as you need, each one representing a subset of the object's features.
Now, even though OLE and C++ share the same binary structure for accessing an object's member functions, the ability to create an object with multiple interfaces is extraordinarily powerful. It represents one of OLE's strongest architectural features and is the basis for its notion of polymorphism between objects in addition to polymorphism between interfaces, which we have already seen.
Because each interface is a group of semantically related functions, each interface that an object supports represents a specific feature of that object. For example, if an object supports the capability of exchanging formatted data structures, it implements the interface named IDataObject, whose member functions describe all the different aspects of exchanging the data, such as set data, get data, enumerate formats, and so on. It is the presence of this interface that describes the object's data exchange capacities.
Herein lies OLE's architectural advantage over single-interface models such as C++: objects can describe their features on a higher level of abstraction than individual member functions, which enables a client to ask the object whether it supports a particular feature before that client attempts to use such a feature. Contrast this to C++ techniques, which might require that a client attempt to call a function simply in order to check whether that function will work. The ability of a client to ask an object about support for a feature decouples the act of testing for functionality from the act of invoking the functionality.
The function IUnknown::QueryInterface (using C++ syntax) is the decoupling mechanism that a client uses to navigate through multiple interfaces. Because QueryInterface belongs to IUnknown, and because all interfaces in OLE are derived from and are polymorphic with IUnknown, QueryInterface is universally available through whatever interface pointer a client might have. The interfaces available through the same implementation of QueryInterface are said to be implemented alongside one another. If you see a reference to "interface A is implemented alongside interface B," it means that you can use QueryInterface to get from A to B and from B to A.
Whenever a client accesses any object, it can always obtain an initial IUnknown pointer for that object. But because the client can only call IUnknown functions through this pointer, it can't do a whole lot with the object. In order to use any other object feature—any other interface—the client must first ask the object whether it supports that feature by calling QueryInterface. In making this call, the client passes the unique identifier a globally unique identifier of the interface it would like to access. If the object supports that interface, it will return a pointer to that interface to the client; otherwise, it returns an error. If support is there, the client is given the exact interface pointer through which it can then call the member functions of that interface and access that feature. If support is not there, the object provides no such pointer, thereby disallowing all calls to unsupported features. An illustration of the QueryInterface process is shown in Figure 1-6.
Figure 1-6.
The QueryInterface function navigates through all the interfaces on an object.
So QueryInterface is a tight coupling between asking an object whether it supports a feature and the ability to access that feature: a client must have an appropriate interface pointer to access a feature, and the only way to obtain the pointer is by calling QueryInterface—you have to ask!
Let this sink in for a while, and you might begin to realize the real importance of this simple mechanism. QueryInterface and the idea that an object's functionality is factored into interfaces rather than single functions provide what is called "robust evolution of functionality over time." This is the ability to take a component and its constituent objects, add new objects to the component and new interfaces to its objects, and redeploy the component into a running system without breaking compatibility with existing clients. Because new features are added in the form of new uniquely identified interfaces and existing clients will never ask for interfaces that they do not understand, new interfaces do not interfere with any existing interfaces. As far as existing clients are concerned, the objects have not changed, but new clients that do understand those additional interfaces can take full advantage of the new features. In this way, you have components and clients that can evolve independently over time, through many revisions, retaining full compatibility with the past without stunting future improvements.
A key point in all of this is that a change to an object or a component requires absolutely no recompilation or changes whatsoever to existing clients. Contrast this with a change to a C++ base class, which always requires a recompilation of any derived classes. In OLE, you never have to update an object client just because the object changes. Certainly any existing client will not use the new features of the object, but as soon as that client is independently updated to ask for those new interfaces, it can take advantage of supporting objects immediately without requiring any changes to the objects themselves.
We'll see a concrete example of this sort of independent evolution of an object and a client in Chapter 2. What is still left to mention here is the idea of polymorphism between objects. Because an interface is defined as a fixed group of member functions, an interface implemented on any object has exactly the same functions and semantics as the same interface implemented on any other object. In other words, a client can call those member functions without having to know exactly what type of object it's really talking to. This is polymorphism: any two objects are polymorphic through the interfaces they have in common. The client can always call each member function in the interface, but what the object does in response to the call can vary within the design of the interface function. For example, with the function IViewObject2::Draw, a client can ask an object to draw its visual presentation to the screen or a printer. What the object will actually draw depends on the object and its state data, but the client's intent to draw the object is constant.
OLE takes advantage of this polymorphism in a number of its higher-level technologies, such as those dealing with compound documents and custom controls. Every OLE control, for example, supports the same set of interfaces that define a control, and so a control container—a client that specifically works with controls—treats all controls polymorphically through those interfaces. The container need not care about the specific types of controls: they're all just controls.
The notion of multiple interfaces for OLE objects has tremendous power and implications. To my knowledge, this extraordinary facility is not part of any other object model, programming language, or operating system technology. The idea that you can factor an object's capabilities into distinct, feature-oriented groups opens all sorts of opportunities to create innovative components and offers a promising future of interoperability and integration between components that has never before been realized.