Polymorphism is many things in object-oriented programming. It is the capability to treat objects from multiple classes identically because they all share one or more interfaces in common. Polymorphism means that heterogeneous objects can respond to the same interface calls from the same client, allowing that client to be written according to a prototype instead of for specific object classes. In addition, polymorphism means that you create more instances (types) of a prototype so existing clients that understand the prototype can immediately use those new classes. In OLE, objects whose classes have a common set of interfaces are polymorphic with one another, as we saw with the recent example concerning the evolution of file rendering objects.
In OLE, QueryInterface and the idea of multiple interfaces provide polymorphism. In C++, you would define basic characteristics in a base class and add characteristics by creating a derived class from that base class. You can derive further specific classes from the first derived class, creating a deep object hierarchy in which a derived class is polymorphic with all classes above it, all the way to the base class. In OLE, however, basic characteristics are expressed in one interface, and additional and more specific sets of characteristics are expressed through additional interfaces. Two object classes that support the same interfaces are like each other—polymorphic—across those interfaces. The classes are instances of the same prototype.
When you start designing interfaces for different sets of characteristics, you have two choices: you can actually use inheritance to derive the more specific interfaces from the more generic ones, or you can define completely separate interfaces for each set of characteristics. Which approach is better?
As an example, let's say I want to model some animals as objects, specifically rabbits and koalas. Since both are animals, I could make a base interface to represent the characteristics of a general animal (which, of course, includes IUnknown members).
interface IAnimal : IUnknown
{
HRESULT Eat(...);
HRESULT Sleep(...);
HRESULT Procreate(...);
}
Here I'm saying that all animals, including humans, share the basic characteristics of eating, sleeping, and procreation. (Arguments to these functions are irrelevant to this discussion.) Now I need to create my IRabbit and IKoala interfaces. (Ideally you'd probably make IRodent and IMarsupial interfaces as well, but we'll keep things simple here.) One way to create these new interfaces would be to use C++ inheritance to derive each new interface from IAnimal:
interface IRabbit : IAnimal
{
HRESULT RaidGardens(...);
HRESULT Hop(...);
HRESULT DigWarrens(...);
}
interface IKoala : IAnimal
{
HRESULT ClimbEucalyptusTrees(...);
HRESULT PouchOpensDown(...);
HRESULT SleepForHoursAfterEating(...);
}
This technique is entirely workable, but it has a significant drawback. If for some reason a new interface, IAnimal2, is created (to add the function Locomotion or some such), it would force the creation of IRabbit2 and IKoala2 if we wanted to update objects to also support IAnimal2. In other words, the change to the base has to propagate to derived interfaces. While this is not a big deal for a two-level inheritance tree, it becomes an utter nightmare with a deep inheritance tree—a change in a base interface might force changes in hundreds of other interfaces! The additional impact of such sweeping changes is that clients and objects using these interfaces would then have to make much more sweeping changes as well.6
The preferred technique is to simply define each additional interface separately, deriving only from IUnknown:
interface IRabbit : IUnknown
{
HRESULT RaidGardens(...);
HRESULT Hop(...);
HRESULT DigWarrens(...);
}
interface IKoala : IUnknown
{
HRESULT ClimbEucalyptusTrees(...);
HRESULT PouchOpensDown(...);
HRESULT SleepForHoursAfterEating(...);
}
An object that implements IAnimal and IRabbit separately, as would be required in this technique, is functionally equivalent to one that implements a single IRabbit derived from IAnimal.7 However, if IAnimal2 comes out, this Rabbit object needs to amend only one of its interface implementations, leaving all others as is. This is beneficial when the object has a large number of interfaces. A client can also rest easy: to exploit IAnimal2, it needs only to modify its code that handles IAnimal to work with IAnimal2, and it can leave all IRabbit calling code unmodified.
I will point out here as well that a deep inheritance tree would be absolutely required if an object could not support multiple interfaces. It is due entirely to QueryInterface that we can avoid the complexity and difficulties associated with deep inheritance. A shallow interface inheritance tree is much easier to work with over time, for objects as well as for clients. Functionally it is identical to a deep inheritance tree but far more practical.
Another drawback to a deep inheritance tree is that it is less efficient to reuse some other component's implementation of base interfaces. But that requires us to look at the reusability of interface implementations.
6 There are only a few cases in OLE in which interface inheritance goes deeper than one level a few of which the original OLE designers acknowledge as less than optimal choices. |
7 Note that the derived IRabbit and the independent IRabbit are not the same interface because they have different vtable layouts and therefore must have different IIDs. Functionally equivalent means that the same features are available in both interfaces. |