Reusability Through Aggregation

Let's now say that we are planning to revise our TextRender object at a later time than out initial containment implementation in the previous section. At that time we find that the implementor of the TextImage object at the time the implementor of the TextRender object sat down to work (or perhaps is making a revision of his object) that the vendor of the TextImage object has improved TextImage such that it implements everything that TextRender would like to do through its IDataObject interface. That is, TextImage still accepts text through SetData but has recently added the ability to make copies of its text and provide those copies through GetData in addition to metafiles and bitmaps.

In this case, the implementor of TextRender now sees that TextImage's implementation of IDataObject is exactly the implementation that TextRender requires. What we, as the implementors of TextRender, would like to do now is simply expose TextImage's IDataObject as our own as shown in Figure 6-4.

Figure 6-4. When an inner object does a complete job implementing an interface, outer objects may want to expose the interface directly.

The only catch is that we must implement the proper behavior of the IUnknown members in the inner object's (TextImage) IDataObject interface: AddRef and Release have to affect the reference count on the outer object (TextRender) and not the reference count of the inner object. Furthermore, QueryInterface has to be able to return the TextRender object's IPersistFile interface. The solution is to inform the inner object that it is being used in an aggregation such that when it sees IUnknown calls to its interfaces it can delegate those calls to the outer object.

One other catch remains: the outer object must have a means to control the lifetime of the inner object through AddRef and Release as well as have a means to query for the interfaces that only exist on the inner object. For that reason, the inner object must implement an isolated version of IUnknown that controls the inner object exclusively and never delegates to the outer object.9. This requires that the inner object separates the IUnknown members of its functional interfaces from an implementation of IUnknown that strictly controls the inner object itself. In other words, the inner object, to support aggregation, must implement two sets of IUnknown functions: delegating and non-delegating.

This, then, is the mechanism for making aggregation work:

  1. When creating the inner object, the outer object must pass its own IUnknown to the inner object through the pUnkOuter parameter of IClassFactory::CreateInstance. pUnkOuter in this case is called the controlling unknown.
  2. The inner object must check pUnkOuter in its implementation of CreateInstance. If this parameter is non-NULL, then the inner object knows it is being created as part of an aggregate. If the inner object does not support aggregation, then it must fail with CLASS_E_NOAGGREGATION. If aggregation is supported, the inner object saves pUnkOuter for later use, but does not call AddRef on it. The reason is that the inner object's lifetime is entirely contained within the outer object's lifetime, so there is no need for the call and to do so would create a circular reference.
  3. If the inner object detects a non-NULL pUnkOuter in CreateInstance, and the call requests the interface IUnknown itself (as is almost always the case), the inner object must be sure to return its non-delegating IUnknown.
  4. If the inner object itself aggregates other objects (which is unknown to the outer object) it must pass the same pUnkOuter pointer it receives down to the next inner object.
  5. When the outer object is queried for an interface it exposes from the inner object, the outer object calls QueryInterface in the non-delegating IUnknown to obtain the pointer to return to the client.
  6. The inner object must delegate to the controlling unknown, that is, pUnkOuter, all IUnknown calls occurring in any interface it implements other than the non-delegating IUnknown.
Through these steps, the inner object is made aware of the outer object, obtains an IUnknown to which it can delegate calls to insure proper behavior of reference counting and QueryInterface, and provides a way for the outer object to control the inner object's lifetime separately. The mechanism is illustrated in Figure 6-5.

Figure 6-5. Aggregation requires an explicit implementation of IUnknown on the inner object and delegation of IUnknown function of any other interface to the outer object's IUnknown functions.

Now let's look at how this mechanism manifests in code. First off, the TextRender object no longer needs its own IDataObject implementation and can thus remove it from its class, but will need to add a member m_pUnkImage to maintain the TextImage's non-delegating IUnknown:


class CTextRender : public IPersistFile {
   private:
      ULONG      m_cRef;         //Reference Count
      char *      m_pszText;      //Pointer to allocated text
      ULONG      m_cchText;      //Number of characters in m_pszText

      IUnknown *   m_pUnkImage;      //TextImage IUnknown
      //Other internal member functions here

   public:
      [Constructor, Destructor]

      //Outer object IUnknown
      HRESULT QueryInterface(REFIID iid, void ** ppv);
      ULONG AddRef(void);
      ULONG Release(void);

      //IPersistFile Member overrides
      ...
   };

In the previous section we saw how the TextRender object would create a TextImage object for containment using CoCreateInstance with the pUnkOuter parameter set to NULL. In aggregation, this parameter will be TextRender's own IUnknown (obtained using a typecast). Furthermore, TextRender must request IUnknown initially from TextImage (storing the pointer in m_pUnkImage):


//TextRender initialization
HRESULT     hr;
hr=CoCreateInstance(CLSID_TextImage, CLSCTX_ SERVER, (IUnknown *)this, IID_IUnknown, (void *)&m_pUnkImage);
if (FAILED(hr))
   //TextImage not available, either fail or disable graphic rendering
//Success:  can now make use of TextImage object.

Now, since TextRender does not have its own IDataObject any longer, its implementation of QueryInterface will use m_pUnkImage to obtain interface pointers:


HRESULT CTextRender::QueryInterface(REFIID iid, void ** ppv) {
   *ppv=NULL;

   //This code assumes an overloaded == operator for GUIDs exists
   if (IID_IUnknown==iid)
      *ppv=(void *)(IUnknown *)this;

   if (IID_IPersitFile==iid)
      *ppv=(void *)(IPersistFile *)this;

   if (IID_IDataObject==iid)
      return m_pUnkImage->QueryInterface(iid, ppv);

   if (NULL==*ppv)
      return E_NOINTERFACE;      //iid not supported.
   
   //Any call to anyone's AddRef is our own.
   AddRef();
   return NOERROR;
   }

Note that delegating QueryInterface to the inner object is done only for those interfaces that the outer object knows it wants to expose. The outer object should not delegate the query as a default case, for such blind forwarding without an understanding of the semantic being forwarded will almost assuredly break the outer object should the inner one be revised with new functionality.