One of the principal benefits of object-oriented software development is that it provides a structure that enables code reuse. After you have created an object that performs a well-defined service in an efficient and error-free manner, you will probably want to reuse that object many times. In this lesson, you will learn about the different ways in which COM objects can use the services of other COM objects. In particular, this includes a discussion of two common reuse techniques, containment and aggregation. These techniques allow you to embed existing COM objects within the COM objects you develop, exposing the interfaces of the embedded objects as interfaces of the containing object.
After this lesson, you will be able to:Estimated lesson time: 30 minutes
- Describe the difference between implementation inheritance and interface inheritance.
- Describe the difference between containment and aggregation as methods of object reuse.
- Describe how to implement the IUnknown interface of an aggregatable object.
- Describe how aggregation is implemented by ATL.
C++ programmers generally reuse existing class definitions using either the containment or inheritance techniques. Containment involves the declaration of an object within a class scope, as shown in the following example:
#include "Acme.h" // Contains AcmeViewer class definition // Defines the member functions SetFile() and Display() class MyViewer { protected: AcmeViewer m_obj; public: MyViewer() {m_obj.SetFile("C:\images\scully.gif", AV_TYPE_GIF);} DisplayGif() {m_obj.Display();} } |
This code defines a simple class called MyViewer, which contains an instance of the pre-existing AcmeViewer class. MyViewer reuses the code contained in the AcmeViewer class. The MyViewer constructor initializes the contained AcmeViewer object, and the MyViewer::DisplayGif() function uses services provided by the AcmeViewer object. The MyViewer class can control access to the AcmeViewer object and the way in which the object's services are used.
Inheritance is a powerful reuse technique that is central to the C++ language. Given what you already know about the AcmeViewer class, it should be obvious that the following declaration:
#include "Acme.h" class MyViewer : public AcmeViewer { public: MyViewer() {SetFile("C:\images\scully.gif", AV_TYPE_GIF);} } |
will allow you to call public or protected member functions of the AcmeViewer class as member functions of a MyViewer object, as follows:
MyViewer aViewer; aViewer.Display(); |
COM supports containment, but it does not support inheritance in the same way that C++ does. In the example just given, the MyViewer class inherits functionality implemented by the AcmeViewer class. The public or protected member functions of the AcmeViewer class are available to the MyViewer class. This kind of inheritance is known as implementation inheritance.
COM does not support implementation inheritance. COM makes a strict logical distinction between the interface that a COM object provides and the object's implementation of that interface. A published COM interface, which is identified around the world by its GUID, is immutable—it is guaranteed never to change. Although there might be many COM objects that implement the functionality described by the interface in many different ways, clients will always know how to communicate with any of these objects. An interface represents a contract between a COM server and a client that specifies what kind of data the object is expecting and what kind of data it will return.
Implementation inheritance causes the implementation of a derived class to be dependent on the implementation of its base class. If the implementation of the base class changes, the derived classes might cease to function properly and would, therefore, need to be re-implemented. This can cause serious problems in large-scale development, particularly if you do not have access to the source code of the base classes. The separation between interface and implementation that COM provides helps to alleviate these kind of problems; but it also means that you cannot reuse COM components by deriving one from another as you can with C++ classes.
COM does support a form of inheritance known as interface inheritance. Interfaces in C++ are implemented as abstract classes containing only pure virtual functions that specify, but do not implement, the interface methods. By deriving one interface from another, you specify the structure of the vtable that will hold pointers to the instantiated methods. For example, the following definition will create a properly structured vtable that contains pointers to the three IUnknown methods, followed by pointers to the methods defined by IEncoder:
IEncoder : public IUnknown { // IEncoder methods declared here } |
It is the immutability of COM interfaces that makes interface inheritance possible. You can derive your COM interface from any other COM interface and be assured that no one will change the definition of the parent interface without your knowledge and thus mess up your vtable.
Developers of COM objects reuse existing COM objects by using either containment or aggregation techniques. Containment and aggregation depend on a relationship in which one object (referred to as the outer object) reuses another object (referred to as the inner object).
COM containment works in a similar manner to the C++ containment technique discussed at the beginning of this lesson. The outer object creates an inner object (typically by calling CoCreateInstance()) and stores a reference to the inner object as a data member. The outer object implements the interfaces of the inner object through stub interfaces that forward method calls to the inner object.
Figure 10.1 shows how a COM object with the ICar interface contains a COM object with the IVehicle interface and how the outer object can expose both interfaces.
Figure 10.1 COM containment
The outer object is not required to expose all interfaces of the inner object. Just as with C++ containment, the interface on the outer object can control access to the inner object, and can also control the way in which the inner object's services are used. The outer object does not forward calls to any of the IUnknown interface methods of the inner object because the inner object does not have any knowledge of the interfaces exposed by the outer object. For example, if a client of the object depicted in Fig 10.1 requested an IUnknown pointer, it would expect to be able to use this pointer to access the ICar interface as well as the IVehicle interface. A pointer to the IUnknown interface of the inner object would not be able to service requests for an ICar pointer.
As with containment, the outer object stores a reference to the IUnknown interface of the inner object. Unlike containment, however, the outer object exposes interfaces of the inner object directly to the client, rather than implementing interface stubs for them. This means that aggregation does not incur the overhead of forwarding method calls imposed by containment.
Figure 10.2 shows how the interface of an inner object is exposed through aggregation.
Figure 10.2 COM aggregation
For aggregation to work, the inner object must be aggregatable—it must be written to support aggregation. Because clients of the outer object can now obtain interface pointers exposed by the inner object, they are able to call the IUnknown interface methods of the inner object. Because the inner object will not be able to service calls to QueryInterface(), AddRef() and Release() on behalf of the outer object, client calls to the inner object's IUnknown interface must be delegated to the outer object's IUnknown interface (the controlling unknown).
NOTE
Bear in mind that a client of an aggregated object knows nothing of the aggregation, which as far as the client is concerned, is implementation detail. The client sees a single COM object that exposes a single IUnknown pointer. If the client receives pointers to interfaces of the inner object, it will assume that they are interfaces exposed by the outer object.
When the outer object creates the inner object, it uses the second argument of the CoCreateInstance() function to pass the address of the controlling unknown to the inner object's class factory. If this address is not NULL, the inner object knows that it is being aggregated and delegates IUnknown method calls from external clients to the controlling unknown.
However, the inner object must be able to handle IUnknown method calls from external clients (which are intended for the controlling unknown) differently from IUnknown method calls that originate from the controlling unknown (which are used by the outer object to discover the interfaces and control the lifetime of the aggregated object). This means that an aggregatable COM object must provide two versions of the IUnknown interface: a delegating version and a non-delegating version. The outer component calls the non-delegating IUnknown methods; external clients call the delegating IUnknown methods. The delegating methods redirect the calls to the controlling unknown (if the object is aggregated), or to the non-delegating unknown (if the object is not aggregated).
ATL provides you with a number of macros and base-class methods that facilitate the implementation of component aggregation. To create an aggregatable (inner) object, you use the ATL Object Wizard to add a new COM object to an ATL project and specify that the component is to support aggregation. You select the appropriate option on the Attributes page, as shown in Figure 10.3.
Figure 10.3 ATL Object Wizard aggregation options
By default, ATL COM objects are created as aggregatable. Selecting the No option in the aggregation group will create a non-aggregatable object. The Only option will create an object that can be used only as an aggregatable object.
HRESULT FinalConstruct() { return CoCreateInstance(CLSID_InnerObject, GetControllingUnknown(), CLSCTX_ALL, IID_IInnerObject, &m_pUnkInner); } |
C++ developers are accustomed to using implementation inheritance as a code reuse technique. COM makes a strict logical distinction between interface and implementation and insists that interfaces remain immutable around the world. This means that COM can support only interface inheritance (reuse of the interface specification) and not implementation inheritance.
COM supports two forms of object reuse: aggregation and containment. These techniques depend on a relationship in which an outer object reuses an inner object. When an inner object is contained, its interfaces are not exposed directly to a client but instead are mediated through stub interfaces implemented by the outer object. An aggregated object exposes its interfaces directly to the client. When aggregating objects, you must implement the inner and outer objects so that a client will be able to access only the controlling unknown—a pointer to the IUnknown interface of the outer object. ATL provides you with a number of macros and base-class methods that facilitate the implementation of component aggregation.