Summary

In OLE, a client deals with every object through one or more interface pointers. The most important question for a client is how it obtains its first pointer to any given object, which depends greatly on the way one identifies an object. In all cases, object classes are uniquely identified and will often use globally unique identifiers (GUIDs) that are also used for precisely identifying unique interfaces. These identifiers are generated with an algorithm that eliminates the chance of collision, and OLE provides various functions for both generating and manipulating these identifiers. In addition, there are rules for creating registry entries for a class identifier in which an object class can describe itself and its server.

Interfaces themselves, besides having a unique identity, are the channels of communication to an object. They are, in fact, the only way a client can know an object. Interfaces are defined as abstract base classes in C++ and as data structures in C. Interfaces can be defined in IDL. The standard return type for interface members is HRESULT, which is used in most cases but is not required. The HRESULT allows an interface member to return multiple success and error codes. Interfaces also have a number of important attributes: they are not classes or objects, but they are strongly typed groups of function signatures that an object must implement to communicate with clients. Interfaces are also immutable in that modifying an interface requires the assignment of a new unique identifier.

The most fundamental interface in OLE is IUnknown, which provides for reference counting and interface negotiation. Reference counting is the way a client controls an object's lifetime through the members AddRef and Release, where specific rules govern when these functions must be called and by whom. Interface negotiation is handled through the member QueryInterface, through which a client asks an object about its support for a feature by asking the object to return an interface pointer that represents the feature in question. Querying an object for such information solves versioning and compatibility problems, allowing for what is called "robust evolution of functionality over time."

An object's implementation is always encapsulated behind its interfaces, and when different object classes implement the same interface, those classes are polymorphic through that interface. If classes share a common set of interfaces, those classes are polymorphic through a larger prototype definition. In addition, individual interfaces that share common member functions are also polymorphic, and as all interfaces in OLE are derived from IUnknown, the AddRef, Release, and QueryInterface functions are ubiquitously available. The interface structure also supports two methods for achieving object reuse. These are called containment and aggregation. Aggregation is an optimization to containment in which one object can directly expose the interface pointer of another provided certain standard rules are followed. Overall, interfaces support the fundamental object-oriented notions of encapsulation, polymorphism, and reusability. Inheritance is a programming language–specific device for achieving the latter two, which is not practical in the domain of binary OLE components.

This chapter has explored these concepts in sample code and has also shown the need for clients and components to initialize the OLE libraries with CoInitialize or OleInitialize. It has shown how to work with OLE in both C and C++ through an exploration of OLE's standard task memory service and through the idea of enumerator objects. It has demonstrated three techniques for implementing an object with multiple interfaces and has illustrated how containment and aggregation work with reusable components.