How to Keep Clients Happy: The Published Interface

 < Free Open Study > 



Think about this contract in the terms of a C++ class library. Have you ever worked with a team using an in-house class library? What happens when some rogue programmer decides to check out some files from source code control and changes the method names, parameter arguments, or worse yet the name of the class? Of course, each and every line of code using that object instance is broken, and massive recompiles ensue. Interface- based programming explicitly states that once an interface is published (which may be defined as at least one object supports it and one client is using it), it becomes a fixed constant in the universe. If we did not enforce this rule, we will break clients, just like the rogue programmer.

The notion of the interface contract is extremely important in COM development, as the physical distance between a client and the object may span different processes or even different machines. If some programmer in sunny California changes an interface of some COM object you are using from frigid Minnesota, we have problems.

Of course, over time, interfaces will need to adapt. An interface such as IDraw may work for the time being, but what if you wish to extend the functionality of this interface to support drawing to an off-screen buffer or to render the image to a printer? In other words, how can we extend an object's functionality without breaking existing clients?

Versioning Published Interfaces

Once an interface is published, we cannot change it in any way. IDraw is a good start to a set of drawing related behaviors, but what if we wish to add in the support mentioned above? The standard way to extend an interface is to assemble a new interface deriving from an existing one. This technique is called interface inheritance. Realize that unlike classical inheritance, we do not inherit any state or implementation, rather we are simply augmenting an existing interface layout with our own.

The common naming convention used when extending an interface is to append numerical suffixes to the root name of the original interface (IDraw, IDraw2, IDraw3, ..., IDraw44). Here then is IDraw2, which derives from IDraw, and therefore inherits the pure virtual Draw() method:

// Interface inheritance interface IDraw2 : public IDraw {      virtual void DrawToMemory() = 0;      virtual void DrawToPrinter() = 0; };

As seen in Figure 2-7, the layout for IDraw2 would be appended to that of IDraw:

click to expand
Figure 2-7: Layout of IDraw2.

Now assume that IDraw2 has become versioned as well. If we wish to safely extend IDraw2 to support the ability to draw to a metafile, we might write the following interface:

// Extending IDraw one level further... interface IDraw3 : public IDraw2 {      virtual void DrawToMetaFile() = 0; };

This gives us the following layout for IDraw3:

click to expand
Figure 2-8: Layout of IDraw3.

Now, what if we wish to create a class that supports all drawing behaviors defined by each of the interfaces above? We simply write a class definition such as:

// Brings in the layout of IDraw, IDraw2 & IDraw3 class C3DRectEx : public IDraw3 { public:      // IDraw + IDraw2 + IDraw3      virtual void Draw();      virtual void DrawToMemory();      virtual void DrawToPrinter();      virtual void DrawToMetaFile(); };

Notice that our class derives only from the nth-most (most derived) interface in the inheritance chain which indirectly brings in the functionality up the inheritance chain. You might be tempted to set up the class definition to look like this:

// This will not work... class CMegaDrawer : public IDraw, public IDraw2, public IDraw3 {      // Humm... };

However, you will not compile, as IDraw3 would hide the shared members of IDraw2, and IDraw2 would hide the members of IDraw (this is a C++-ism, not a restriction imposed by interface-based development). C++ does allow virtual inheritance to avoid this very situation; however, it is not as clean (or obvious) as simply directly deriving from the nth-most interface. This is the standard approach used in COM, so we'll stick to it.

So, to summarize the correct way to version interfaces: When versioning a published interface, create interface hierarchies using numerical suffixes to mark a logical and safe extension. The class implementing the members in the chain simply implements the most derived member.

The Wrong Way to Version Interfaces

One question that might come about is "Why not just tack on additional methods to an existing interface?" Why bother with IDraw, IDraw2, and IDraw3 as individuals if we could simply append additional methods to the original IDraw interface:

// A poorly versioned interface. interface IDraw {      // Original IDraw: Developed 3/24/99      virtual void Draw() = 0;      // Extension 2: Modified 7/13/99      virtual void DrawToMemory() = 0;      virtual void DrawToPrinter() = 0;      // Extension 3: Modified 9/2/99      virtual void DrawToMetaFile() = 0; };

This seems correct. You might assume that existing clients using the original iteration of IDraw (which contained Draw() only) would never call the second and third extensions, and therefore would never break. You're right! Technically, you could preserve existing client code when extending the original IDraw interface. These clients would be blissfully unaware of any method beyond Draw(), and their code base would reflect that fact. But how about the other way around?

Imagine a brand new client has activated an object supporting the latest and greatest version of IDraw (containing all four methods). This client naturally assumes that all objects supporting IDraw support the same layout. After all, the interface contract states that once a server supports an interface, it cannot modify the interface in any way.

Now the same client creates another object also supporting IDraw; however, this particular object only defines IDraw supporting an order of the first extension. If the client calls DrawToMetaFile(), it is making calls into sweet oblivion, and a crash is imminent. We have just broken polymorphism, as seen in Figure 2-9.

click to expand
Figure 2-9: Non-standard versioning == loss of polymorphism.

Of course, during the development cycle you will add and remove methods from the interfaces you are defining. Versioning an interface comes into play when you have published a given interface and wish to safely augment it, as specified by the contract.

Developing Interface Hierarchies

Hopefully you agree that we should always use standard interface versioning to safely extend a given interface. On a similar note, interface-based programming also gives you the ability to create hierarchies of interfaces to describe the relationships between related behaviors. This has the same flavor as building a traditional class library with the "no state, no implementation" provision.

Imagine you wanted to create a number of interfaces to represent different types of automobile behaviors. ICar may be a base interface that defines the standard behavior of any car, and may have pure virtual methods such as Go(), Stop(), WipeWindow(), and so forth. From ICar, you may derive other interfaces to model specific car types. I'll leave it to you to dream up the methods defined in each interface as you ponder Figure 2-10.

click to expand
Figure 2-10: A hierarchy of related interfaces.

In C++, we can code the above interface hierarchy as such:

// The base interface: ICar. interface ICar {      // All the ICar methods... } // ISportsCar. interface ISportsCar : public ICar {      // ICar and ISportsCar methods... } // IReallyFastSportsCar. interface IReallyFastSportsCar: public ISportsCar {      // ICar, ISportsCar and IReallyFastSportsCar methods... } // IFamilyCar. interface IFamilyCar: public ICar {      // ICar and IFamilyCar methods... } // IBoringFamilyCar. interface IBoringFamilyCar: public IFamilyCar {      // ICar, IFamilyCar and IBoringFamilyCar methods... }

As before, a class wishing to support these behaviors would derive from the nth-most interface in the hierarchy, and acquire all functionality up the inheritance chain. For example, we may wish to model a standard sliding-door automobile as so:

// CMiniVan implements ICar, IFamilyCar and IBoringFamilyCar. class CMiniVan : public IBoringFamilyCar { public:      // ICar methods.      // IFamilyCar methods.      // IBoringFamilyCar methods. };

We also may wish to develop a more exciting automobile by deriving from ISportsCar:

// This automobile implements ICar and ISportsCar class CSportsSedan : public ISportsCar { public:      // ICar methods.      // ISportsCar methods. };

When you are developing interface hierarchies, you are able to suggest an Is-A relationship within your objects. Without any offense intended: A minivan Is-A boring family car that Is-A family car which Is-A car.



 < 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