Lesson 2: Reusing COM Objects

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:

  • 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.
Estimated lesson time: 30 minutes

Object Reuse in C++ and COM

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.

Containment and Aggregation

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).

Containment

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.

click to view at full size.

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.

Aggregation

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.

click to view at full size.

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).

Implementing Aggregation Using ATL

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.

click to view at full size.

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.

  • To implement an outer object:
    1. Add an IUnknown pointer to your class object and initialize it to NULL in the constructor. For the purposes of this example, we will refer to this pointer as m_pUnkInner.
    2. Inside your class definition, declare the macro DECLARE_GET_ CONTROLLING_UNKNOWN. This macro defines a function named GetControllingUnknown().
    3. Override the methods FinalConstruct() and FinalRelease(). These are methods of the CComObjectRootBase base class, which are called as the last steps for creating and destroying a COM object.
    4. In FinalConstruct(), you call CoCreateInstance(), passing the CLSID of the inner object you want to create as the first argument. Pass the return value of the GetControllingUnknown() function as the second argument to supply the inner class with the controlling IUnknown pointer. Pass the address of the m_pUnkInner pointer as the fifth argument, to receive a pointer of the inner object's IUnknown interface. An example of an overridden FinalConstruct() function follows:
    5. HRESULT FinalConstruct() {     return CoCreateInstance(CLSID_InnerObject,         GetControllingUnknown(), CLSCTX_ALL,         IID_IInnerObject, &m_pUnkInner); }

    6. In FinalRelease, call IUnknown::Release() on the inner object.
    7. Using the COM_INTERFACE_ENTRY_AGGREGATE macro, add an entry to the outer object's COM map for the interface of the inner object. This is a specialization of the COM_INTERFACE_ENTRY macro that handles aggregated objects. COM_INTERFACE_ENTRY_AGGREGATE takes an extra IUnknown * argument that points to the inner unknown.
    8. Add the interface definition for the inner object to the .idl file of the outer object, and reference that interface in the [coclass] section.

    Lesson Summary

    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.



    Microsoft Press - Desktop Applications with Microsoft Visual C++ 6. 0. MCSD Training Kit
    Desktop Applications with Microsoft Visual C++ 6.0 MCSD Training Kit
    ISBN: 0735607958
    EAN: 2147483647
    Year: 1999
    Pages: 95

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net