Containment and Aggregation in DCOM

Niranjan Ramakrishnan

 

In the last issue we considered the basics of a DCOM interface - IUnknown, AddRef() and Release(). In this article, we will look at how DCOM objects can be reused by other DCOM objects.

Before we go further, let us recall a few DCOM concepts which form the centerpiece of the COM architecture.

Everything in COM/DCOM is based on interfaces. The interface is unchanging, and, once defined, cannot be altered. Clients programs depend on the interface. Any component which wishes to provide services conforming to an interface can do so by implementing the interface.

Now we come to the next question. If I want to write a component which incorporates the services provided by existing COM/DCOM components, how can I do so? The simple answer - that I could just inherit the class of the existing component - has several problems. For one thing, inheriting for code reuse is of dubious merit at best. Second, it ties down the new class to a particular implementation, thus making it non-reusable if someone were to try to create a new component from the one we have created.

The two approaches

Actually, there are two approaches to reuse in COM. Depending on your situation, you can use either one, or even both. Let us take a simple example. We have two interfaces, TransmissionInterface and RadioInterface, which look like this:

TransmissionInterface.h

 class TransmissionInterface {

// pure virtuals QI(), AddRef() and Release()

virtual void turnLeft() = 0;
virtual void turnRight() = 0;

};

 

 

RadioInterface.h

 class RadioInterface {

// pure virtuals QI(), AddRef() and Release()

virtual double setFrequency(double newFreq) = 0;
virtual int setBand(int newBand) = 0;

};

 

There are classes which implement these interfaces, Transmission and Radio, which implement these interfaces. Now to the main point - when I implement a new component called Car, I'd like to use the two components that are created here. I'd like the car to have a radio and a transmission.

In the case of the former, no one ever sees the Transmission directly. They see it indirectly via the Car. The client uses the methods of the car, which in turn calls upon the Transmission. This model is called Containment.

In the case of the Radio, we expose the Radio directly to the client. The client knows there is a Radio which they are using. This model is called Aggregation.

In more general terms, Containment is where the client is talking to the outer object, and is unaware of the inner object. Aggregation is where the client is given access to the inner object, and conducts all further interactions (for that interface) directly with the inner object. We can choose either one. Each has its advantages and disadvantages.

Containment puts the outer object in control. You get to re-implement part or all of the interface provided by the contained object. This can provide you with greater control over the usage of the inner object.

Aggregation has the benefit of least work. You merely return a pointer (in general terms, a reference) to the inner object from the outer object, and the client deals directly with the inner object thereafter.

On the face of it, aggregation wins hands down. In containment, you have to provide an outer interface even if only to act as a post-office forwarding mail to another address. Why would anyone ever use containment? The answer is, there is more complexity to aggregation than meets the eye.

The perils of aggregation
Aggregation means the inner object is accessed directly by the client. Let us suppose the client gets the inner object. Isn't that exactly what the client wants - a reference to something that fulfils the an interface? Sure it is. But...

Remember that the client (unlike the car owner, who physically sees the radio) has no physical knowledge of which component is implementing which interface. The only thing they see is one object, which fulfils a number of interfaces. Thus, the client in this case cannot differentiate between the objects that actually implement an interface. Whether an interface is fulfilled via containment or aggregation makes absolutely no difference to the client, who is oblivious to this distinction. Thus there arises a problem. Let us assume the client obtains a reference to the inner object. The client uses the interface of the inner object, and now inquires from the inner object whether it fulfills some other interface (e.g., CD player). If the inner object responds as itself (Radio), then the answer would be, "No". If on the other hand it responds as the outer object (which is how it should, in this case), the answer would be a yes (assuming the outer object fulfils the CD Player interface). Thus, we need a way by which an inner object, while being dealt with in an aggregated situation, responds as the outer object whenever it is asked about anything outside of its own interface.

In the next article we will discuss how specifically an aggregated object can accomplish this distinction and why aggregation can be an effective means of reuse.