COM Reuse Mechanisms: Containment and Aggregation

 < Free Open Study > 



When you have two COM objects that seem to naturally work together, reuse is achieved when one of the coclasses accounts for the interfaces of another. What makes this form of reuse extremely interesting is that you are reusing a COM object at the binary level. Therefore it is completely possible for a coclass developed in Visual Basic to use a coclass developed in Delphi, Java, or C++. COM provides two ways to allow one object to reuse another: containment and aggregation.

Containment and Delegation in C++

One approach to achieve binary reuse is COM containment. This is a very flexible technique, as a COM object can contain other COM objects contained in EXE or DLL servers (aggregation is more restrictive, as we will see). As a C++ developer, you use containment so often that you most likely don't even notice it. Consider a simple C++ class named CCar, using no COM whatsoever. As you are creating this class, you wish to account for the fact that the CCar "has a" CRadio. This relationship may be coded in C++ as the following:

// Here is a radio. class CRadio { public:      void On();      void Off();      IncreaseVolume(int db); }; // A car "has-a" radio. class CCar { public:      void TurnOnRadio(bool state);      IncreaseVolume(int db);      void Accelerate();      void Stop(); private:      CRadio m_theRadio; };

As the CRadio object is a private data member of CCar, object users cannot access the methods of CRadio directly. Instead, the object user indirectly manipulates the inner object by accessing methods of the outer object (CCar). This is where delegation comes into play. When you establish a Has-A relationship among your objects, you decide how the outer class exposes the functionality of the inner class(es). For example, you may implement CCar::TurnOnRadio() as the following:

// The TurnOnRadio method manipulates the inner CRadio object. // Clients have no clue that CCar operates internally on a CRadio instance. void CCar::TurnOnRadio(bool state) {      if(state == true)           m_theRadio.On();      else           m_theRadio.Off(); }

The important thing to notice here is that we did not extend the public interface of CCar with On() and Off() methods, although we would be free to do so. Instead, we have a single method (TurnOnRadio()) that accesses two methods of CRadio. When delegating, we may choose to have a one-to-one correspondence between the inner and outer methods as so:

// Just forward the call to the inner object. void CCar::IncreaseVolume(int db) {      m_theRadio.IncreaseVolume(db); }

Finally, we can implement methods of outer classes that make no use at all of any inner objects:

// This method of CCar makes no use of the inner CRadio. void CCar::Accelerate () {      // Accelerate logic. }

What does this have to do with COM containment, you ask? Everything. In C++, inner objects are created using the member initialization list (or by an automatic call to the inner class's default constructor if you forgo the member initialization list). In COM, inner objects are created with COM library calls. The outer COM object has the same choices as the outer C++ object we have just examined, in that it may (or may not) make use of the inner object for a given interface method. The outer COM object may (or may not) expose the same interface as the inner object(s). In short, COM containment allows an outer class to create and use inner classes to help it get its work done.

Containment and Delegation in COM

COM containment allows an outer class to leverage the interfaces of an inner class that is contained in another binary COM server. The outer class is responsible for controlling the lifetime management of the inner objects it is using, as well as deciding how to expose its interfaces of the contained objects to the outside world (if necessary). In many ways, COM containment illustrates the classic Has-A relationship.

Assume that CoHexagon (an outer object) wishes to reuse another COM object named CoColor. CoColor can be found in another binary server (ColorServer.exe) and defines a single method on the default interface: SetRGB(). Here is the IDL:

// The [default] interface of CoColor. [object, uuid(DDF18635-24F6-11D3-B8F9-0020781238D4), oleautomation] interface IColor : IUnknown {      HRESULT SetRGB([in] short red, [in] short green, [in] short blue); };

The first design choice we face is to decide how we wish to expose the inner object's functionality from a CoHexagon instance. One choice is to simply create the inner object and make calls to the inner object behind the scenes, without directly exposing the IColor interface to the outside world. In this light, CoColor is a private helper object of the outer class. This would be analogous to a standard C++ class called CEmployee that contains an internal private CString object to represent the name of an instantiated object. The user of CEmployee has no idea it is maintaining a private CString.

Another approach is to configure CoHexagon to support the interface(s) of the inner object(s). When the outer object implements the methods of the given interfaces, it simply delegates to the implementation of the inner object. For the sake of illustration, assume we have configured CoHexagon to support the IColor interface. When we implement the SetRGB() method, we delegate the call to the inner object. Conceptually, the relationship looks like the following:

click to expand
Figure 8-5: CoColor is contained by CoHexagon.

A Containment/Delegation Example

Creating a contained object is as simple as making a call to CoCreateInstance() or CoGetClassObject(). In the ATL framework, the safest place to create contained objects is within the FinalConstruct() method of the outer class. When we create the inner object, we should ask for any interfaces we require and cache the returned interfaces in private member variables. As well, we will need to release the acquired interfaces during FinalRelease().

// CoHexagon "has-a" CoColor. class ATL_NO_VTABLE CCoHexagon :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CCoHexagon, &CLSID_CoHexagon>,      public IDraw,      public IColor { public:      CCoHexagon() : m_pColor(NULL)      { } ...      // Create the inner object and get the interfaces we need.      HRESULT FinalConstruct()      {           return CoCreateInstance(CLSID_CoColor, NULL, CLSCTX_SERVER,                                 IID_IColor, (void**)&m_pColor);      }      // Release the interfaces we have.      void FinalRelease()      {           if(m_pColor) m_pColor->Release();      } public:      // Storage for the IColor pointer.      IColor* m_pColor;      // The outer class defines IColor, but the implementation details      // are held in the inner object.      STDMETHOD(SetRGB)( short red, short green, short blue)      {           // Delegate to inner object.           m_pColor->SetRGB(red, green, blue);           return S_OK;      } }; 

Containment is a very common way to reuse COM objects, as the inner and outer objects may be contained in either DLL or EXE servers. The key point to note is that an outer object is in control of how "visible" the inner object is to the outside world. Here, CoColor has a single method from a single interface. If CoColor supported five different interfaces, it is up to the outer object to determine which interfaces are directly exposed and which are simply used internally to help the outer object get some work done.

Using a Contained Object

To make use of the contained object, assume we have a simple VB tester. The VB client has no clue that CoHexagon is delegating calls to an inner object.

' Make a red hex. ' Private Sub btnSetColor_Click()      Dim o As New CoHexagon      Dim i As IColor      Set i = o      i.SetRGB 255, 0, 0 End Sub

COM containment is very easy to implement, yet it does have a down side. If you have an outer object using (for example) five interfaces of an inner object, you may end up with a lot of code that looks like the following:

// Containment can create lots of delegation code void COuterClass::SomeMethodOnOuterObject() {      m_pInnerOne->SomeMethodOnInnerObject(); }

Before we examine COM aggregation, the next lab allows you to use containment to develop a new automobile class that leverages your existing CoCar.

Lab 8-4: COM Containment

The Has-A relationship is expressed when an outer (containing) coclass creates, maintains, and destroys an inner (contained) object. In this lab you will create a new coclass named CoMiniVan, which makes use of COM containment to help complete its implementation. This lab will also serve as the basis for the next lab in this chapter, where we will be creating hot rods and Jeeps using COM aggregation.

 On the CD   The solution for this lab can be found on your CD-ROM under:
Labs\Chapter 08\ATLVehicles
Labs\Chapter 08\ATLVehicles\VB Client

Step One: Create the Initial Containing Coclass

Create a brand new ATL project workspace named ATLVehicles (this lab assumes an in-proc server, but a local server would be just fine). Insert a new Simple Object named CoMiniVan, which supports a [default] interface named IMiniVan (be sure to choose a Custom interface). Use the Add Method Wizard to add a single method to the IMiniVan interface named CreateMiniVan():

// We will implement this method in a later step... [object, uuid(E8A4D620-3E44-11d3-B910-0020781238D4), oleautomation] interface IMiniVan : IUnknown {      HRESULT CreateMiniVan([in] BSTR petName, [in] int maxSpeed,                          [in] BSTR ownerName, [in] BSTR ownerAddress,                          [in] int numberOfDoors); };

Notice that this looks quite a bit like the Create() method of the ICreate interface we developed in our tear-off lab, except for the final parameter which represents the number of doors. As you may be guessing, IMiniVan::CreateMiniVan() will simply delegate to ICreate::Create().

Next, add a single property named NumberOfDoors, which will be used to get and set a private short maintained by the minivan (m_numberOfDoors). Be sure to initialize this variable to a safe value in the constructor and implement your new property.

Step Two: Create the Contained Coclass

Now we will add the code to create a contained CoCar. To begin this process, we need to copy over some files from our ATLTearOff project, and place them in our current project directory. Go into the Windows Explorer, and copy the MIDL-generated ATLTearOff.h, ATLTearOff_i.c, and ATLTearOff.idl files into your current project folder. When the CoMiniVan object is created, we in turn create our tear-off version of the ATL CoCar and obtain a number of interfaces from the inner object. To hold these interfaces, add the following member variables to CoMiniVan:

private:      // These hold private interfaces on the 'inner' object.      IUnknown* m_pCarUnk;      ICreate* m_pCreate;      IEngine* m_pEngine;      IStats* m_pStats;

Override the FinalConstruct() method in your CoMiniVan coclass. When using COM containment, you select which interfaces of the inner object you wish to manipulate. We will be creating our ATLCoCar within this method, and obtain ICreate, IEngine, and IStats pointers:

// Have the containing class grab interfaces of the inner class. HRESULT FinalConstruct() {      HRESULT hr;      hr = CoCreateInstance(CLSID_ATLCoCar, NULL, CLSCTX_SERVER,                          IID_IUnknown, (void**)&m_pCarUnk);      if(SUCCEEDED(hr))      {           MessageBox(NULL, "Created contained car",                "Message from Minivan", MB_OK | MB_SETFOREGROUND);           m_pCarUnk->QueryInterface(IID_ICreate, (void**)&m_pCreate);           m_pCarUnk->QueryInterface(IID_IEngine, (void**)&m_pEngine);           m_pCarUnk->QueryInterface(IID_IStats, (void**)&m_pStats);      }      else           return E_FAIL;     // Don't let them make minivan                          // if we can't make a car.      return hr; }

Of course, we need to call Release() on all acquired interfaces when the CoMiniVan is destroyed. Override your FinalRelease() method to do so:

// Free up the inner class. void FinalRelease() {      m_pCarUnk->Release();      m_pCreate->Release();      m_pEngine->Release();      m_pStats->Release(); }

This code correctly models a contained relationship among COM objects. When a client creates a CoMiniVan, you turn around and create the ATLCoCar. Notice, however, that we have not provided a way for the outside world to make use of these internal interfaces. That is a job for delegation.

Step Three: Delegate to the Inner Object

To expose the functionality of the inner object to COM clients, we will derive from the obtained interfaces of the inner object, add entries to the outer object's COM map, and delegate accordingly.

Let's set up CoMiniVan to only allow clients to access the IStats and IEngine interfaces directly, while maintaining the ICreate interface for private internal use. Derive CoMiniVan from IStats and IEngine, and add an entry for each in your COM map:

// Mimic the inner object's interfaces. class ATL_NO_VTABLE CCoMiniVan :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CCoMiniVan, &CLSID_CoMiniVan>,      public IMiniVan,      public IEngine,      public IStats { public: ... BEGIN_COM_MAP(CCoMiniVan)      COM_INTERFACE_ENTRY(IMiniVan)      COM_INTERFACE_ENTRY(IEngine)      COM_INTERFACE_ENTRY(IStats) END_COM_MAP() ... }; 

Recall that IEngine and IStats define the following members, which you will now need to implement in your CoMiniVan. Here are the prototypes:

 // IEngine.      STDMETHOD(GetCurSpeed)(/*[out, retval]*/ int* curSpeed);      STDMETHOD(GetMaxSpeed)(/*[out, retval]*/ int* maxSpeed);      STDMETHOD(SpeedUp)(); // IStats      STDMETHOD(GetPetName)(/*[out, retval]*/ BSTR* petName);      STDMETHOD(DisplayStats)();

The implementation of the IEngine interface will be easy. Simply call the same method on your m_pEngine interface pointer. Here is CoMiniVan::GetCurSpeed() to get you started (do the same type of delegation for GetMaxSpeed() and SpeedUp()):

// We will delegate all methods to the inner object using the correct interface pointer. STDMETHODIMP CCoMiniVan::GetCurSpeed(int *curSpeed) {      return m_pEngine->GetCurSpeed(curSpeed); }

IStats works the same exact way. This time, implement each method, making use of your m_pStats pointer. For example:

// If a client asks me to display stats, I ask the inner object to do so... STDMETHODIMP CCoMiniVan::DisplayStats() {      return m_pStats->DisplayStats(); }

The final item to implement on CoMiniVan is the CreateMiniVan() method of IMiniVan. Here we will make use of the private ICreate interface pointer, and set the private m_NumberOfDoors variable we are maintaining ourselves:

// CreateMiniVan() also delegates. STDMETHODIMP CCoMiniVan::CreateMiniVan(BSTR petName, int maxSpeed,      BSTR ownerName, BSTR ownerAddress, int numberOfDoors) {      // Tell the car to create itself.      m_pCreate-> Create(ownerName, ownerAddress, petName, maxSpeed);      // This is my state data, so I have to hold onto it...      m_numberOfDoors = numberOfDoors;      return S_OK; }

With this, the CoMiniVan is complete. If you are using a C++ client, your work is done. Typically C++ clients do not need to access a *.tlb file to create your objects. But think for a moment about the VB or Java clients of the world. Even though we have added support to CoMiniVan for the IEngine and IStats interface, how is a type library-dependent client able to create variables of these interfaces?

' Help! I can't do this unless IStats is in a type library! ' Dim itfStats as IStats 

The answer is they can't do so until we update our IDL file. That realization brings us to the final step of this lab.

Step Four: Update Your IDL

Although this task sounds trivial, remember you are attempting to support IDL interfaces defined in another IDL file! Luckily we did copy over ATLTearOff.idl in the first part of this lab, so we may import this file as part of our own. Edit your ATLVehicles.idl file as so:

// Need to import the IDL file that defines the inner object's interfaces. import "ATLTearOff.idl"; 

Now, to force the MIDL compiler to publish IEngine and IStats in CoMiniVan's type library, we must forward declare these interfaces in the library statement. Here is the updated IDL:

// Forward declare interfaces to include them in your type information. [ uuid(DDF18619-24F6-11D3-B8F9-0020781238D4), version(1.0), helpstring("ATLVehicles 1.0 Type Library") ] library ATLVEHICLESLib {      importlib("stdole32.tlb");      importlib("stdole2.tlb");      interface IStats;      interface IEngine;      [ uuid(DDF18626-24F6-11D3-B8F9-0020781238D4),        helpstring("CoMiniVan Class") ]      coclass CoMiniVan      {           [default] interface IMiniVan;           interface IStats;           interface IEngine;      }; };

Note that we do not list ICreate, as we do not wish external COM clients to query for this interface from a CoMiniVan instance. We are only using ICreate internally to help with the implementation of CreateMiniVan().

Recompile the server and set a reference to your type library using Visual Basic, and examine your objects (Figure 8-6). You will see things have turned out just fine. Go ahead and create a client of your choice, and move on to the wonderful world of COM aggregation.

click to expand
Figure 8-6: The result of our IDL configuration.



 < Free Open Study > 



Developer's Workshop to COM and ATL 3.0
Developers Workshop to COM and ATL 3.0
ISBN: 1556227043
EAN: 2147483647
Year: 2000
Pages: 171

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