Defining an Interface Using C

 < Free Open Study > 



Defining an Interface Using C++

So then, an interface expresses a single, discrete functionality that a class may choose to support. This functionality is defined by a set of semantically related methods. It is the responsibility of the class, however, to decide exactly how the methods of a given interface will be implemented. The interface is used only to describe the "form" imposed on a class that is supporting it. If this sounds familiar, it should.

Recall that abstract base classes define pure virtual functions—methods that describe the general flavor of a functionality, but provide no implementation. The implementation details are determined by the derived subclass. Consider a C3DRect class inheriting a pure virtual method named Draw() defined in CShape:

// An abstract base class. class CShape { public:      virtual void Draw() = 0;      // Other state data and member functions (public, private or protected). }; // Simple sub class. class C3DRect : public CShape { public:      // Subclasses must implement pure virtual base class functions.      virtual void Draw()      {           // Code to draw 3D rectangle          } }; 

Interfaces, from a syntactical level, are a collection of pure virtual functions (as you may have guessed). Interfaces do not define any state associated with the behavior; in other words, interfaces have no public, private, or protected data sector. However, a class supporting the interface may (and often will) define any number of data types to flesh out the implementation details of the interface methods. As you can see, an interface only describes what can be done. The supporting class defines how it is accomplished.

Also note this: Supporting an interface is an all-or-nothing proposition. The implementing class cannot be selective when supporting interfaces—it is obligated to provide functionality for all defined methods. This should make sense. As an interface is a collection of pure virtual functions, the supporting class must define every member, or else it too will be an abstract class! We can define an interface in C++ quite easily:

// This interface describes generic drawing behavior. interface IDraw {      virtual void Draw() = 0; };

Truth be told, interfaces can be defined in C++ using the class, struct, or interface keywords. The benefit of using the struct or interface keyword is that the default visibility is public, where the default visibility of a class is private. The interface keyword is actually a typedef for struct, as defined in <objbase.h>. This is a core COM header file which we will make use of throughout the book, often indirectly by simply including <windows.h>. Here is the formal definition of interface:

// <objbase.h> // The 'interface' keyword is not an inherit C++ keyword, but a redefinition of the // C++ 'struct' keyword. #define interface struct 

Therefore, you may define the IDraw interface in the following syntax as well, both of which are logically equivalent to the interface keyword, and result in the same physical form (and implied behavior) imposed on the supporting class:

// Defining an interface with the class keyword. // You must use the 'public' specifier when using the class keyword. class IDraw { public:           virtual void Draw() =0; }; // Defining an interface with the struct keyword. // Don't need to use the 'public' specifier, as a struct is automatically public. struct IDraw {      virtual void Draw() = 0; };

In keeping with the spirit of interface-based programming, we will stick with the interface keyword more often than not in this book (just be sure to #include <objbase.h> or <windows.h> in your project workspaces to grab the correct definition).

Supporting Interfaces in C++

When a class wishes to support a behavior specified by an interface, we make use of C++ public inheritance. Supporting a single interface looks like a simple case of subclassing using classical inheritance except this time we inherit no state and no implementation. We only inherit a set of pure virtual functions which we then may implement in a manner unique to our particular class.

As mentioned, a single class may expose any number of interfaces. C++ provides two mechanisms that allow a class to support numerous interfaces: multiple inheritance (MI) and/or a technique called nested classes. Either approach still yields the same result: an ordered layout in memory (vTables) supported by a given class. ATL uses MI to support multiple interfaces, whereas MFC uses the nested class technique. In Chapter 8 we will see how nested classes can be used to resolve method name clashes (for example, what if your class supports two interfaces, each of which defines a method with the same name?). We will examine how to support multiple interfaces using MI, but for now here is the C3DRect class supporting the single IDraw interface, diagrammed in Figure 2-4.

click to expand
Figure 2-4: Our C3DRect class.

We may express this relationship in code as the following:

// C3DRect supports the IDraw interface. class C3DRect : public IDraw { public:      // No longer pure virtual.      void Draw() { // Add your code here. } private:      // Any data points or private helper functions      // the C3DRect class may need to draw itself. };

Because the IDraw interface is completely abstract, C3DRect is now required to implement each method defined within it, which in this case happens to be exactly one. As you recall from Chapter 1, if we do not implement the methods of IDraw, we have a non-creatable abstract class.

Note 

This is worth repeating: What makes interface-based programming unique from traditional object-based development is the key point that interfaces never provide implementation, state data, or private helper functions. This is much unlike a traditional abstract base class which could provide all of the above, as well as a set of virtual functions.

Now that we can create and implement interfaces in C++, we need to take a look at what this new approach to class design gives us. In other words, "Why would I do this?" We have already seen how interface-based classes ease the burden of the object user. Next, let's examine how interfaces give us deeper encapsulation and a new approach to polymorphism.

Interfaces Provide Deeper Encapsulation

A core trait of interface-based programming is that object users can only access the object's functionality through the set of supported interfaces, not from the object instance itself. In COM development, clients never directly create object instances. Instead objects are created behind the curtains using functions of the COM library. These library functions return a requested interface to the client, leaving the user completely encapsulated from the details of the object's implementation, location, and activation.

As you recall from the previous chapter, when a class defines virtual functions, the C++ compiler silently inserts a hidden vPtr data member, which points to the vTable of the object. The vTable in turn points to the address of the function implementation. Every supported interface results in a new vPtr/vTable defined by the class. Interfaces, being a collection of pure virtual functions, enforce a physical layout on the object. To illustrate, let's create an additional interface for our graphical library named IShapeEdit:

// IShapeEdit provides behavior to modify an existing shape. enum FILLTYPE { HATCH = 0, SOLID = 1, POLKADOT = 2 }; interface IShapeEdit {      virtual void Fill (FILLTYPE fType) = 0;     // Solid, hatch or polka dot enum.      virtual void Inverse() = 0;      virtual void Stretch(int factor) = 0; };

This interface allows the object user to manipulate an existing shape in a number of ways. If we extend the functionality of C3DRect to support the IShapeEdit interface, we have now changed the physical layout of its vTables. Using standard multiple inheritance, we may modify the class definition to support IDraw and IShapeEdit as so:

// Added support for another interface using MI. class C3DRect :public IDraw,               public IShapeEdit { public:      // IDraw interface methods.      void Draw();      // IShapeEdit interface methods.      void Fill (FILLTYPE fType);      void Inverse();      void Stretch(int factor); private:      // State data used to give life to interface methods. };

C3DRect now has a layout that looks like the following:

click to expand
Figure 2-5: Internal layout of C3DRect supporting two interfaces.

So what does all this have to do with encapsulation? Interface-based programming provides deeper encapsulation due to the simple fact that when an object user obtains an interface pointer from an object, that interface pointer is actually a pointer to the vPtr which points to the vTable! (Yes, the levels of indirection are staggering at first.) Given Figure 2-5, you can see that if a client has a pointer to IDraw, it may only access the members in the IDraw vTable. Clients holding IShapeEdit* variables have access to members in the IShapeEdit vTable.

If that isn't encapsulation, I am not sure what is. The client only holds a pointer to our class's vTable pointer, which contains no state and no implementation. Interface-based programming makes a clear distinction between the interface and the underlying implementation. In short, the interface is a first-class citizen in COM development. Of course we still have not addressed the issues of:

  • How are these objects created?

  • How does an object user obtain a given interface?

  • How can an object user dispose of an object, if it did not directly create it?

We will be building our own limited interface-based API to address the questions above, mimicking the behavior found in the COM library, but first let's look at how the interface provides us with polymorphic behavior.

Interfaces Provide Polymorphic Behavior

Interfaces provide yet another way to bring polymorphic activity into our systems. Because interfaces provide only the "what" and not the "how" of a set of related methods, every class supporting the interface is allowed to implement the methods on its own terms. In Figure 2-6, we have three classes supporting the IDraw interface:

click to expand
Figure 2-6: Polymorphism with IDraw.

The implementation code used to render the images is of no importance to the interface user. In true COM development, this proposition becomes even more interesting as the objects supporting IDraw may be written in any number of languages, and may be located across the wire on a distant machine. From the user's perspective all that is known is the object supports a well-defined interface for drawing (IDraw) and if calls are made, the object responds.

Even though we have not yet looked at ways to obtain a given interface pointer, assume we have at our disposal some API functions that return IDraw pointers from various objects. An object user may create arrays of these interface pointers, and ask each shape to draw itself using interface-based polymorphism:

 // Create an array of IDraw pointers. IDraw* iFace[3];       // Get IDraw interface from some objects. iFace[0] = GetIDrawFromCircle(); iFace[1] = GetIDrawFromSquare(); iFace[2] = GetIDrawFromTriangle(); // Now draw each one using the IDraw interface. for (int i = 0; i < 3; i++)           iFace[i]->Draw();     

Interface pointers may work as parameters to functions as well, again providing polymorphism. For example, imagine that we have created an instance of each graphic object, and obtained access to the related IDraw interface. We might then write a function that takes a single IDraw pointer as the sole parameter. In this way, the function can operate with any object at all, as long as it supports the IDraw interface:

// This function can be sent any IDraw compatible object. void RenderAShape(IDraw* pdrawObj) {      // Call the Draw method of the IDraw interface.      pdrawObj->Draw(); }

Declaring Interface Variables

You should understand that we could never make an instance of an interface, just as we cannot make an instance of an abstract base class. In either case, however, we can make pointers to them. Therefore, you will never see code like this in the COM universe (in fact, you won't even compile):

// Can't make an instance of an interface! IDraw theDrawer;          // Error! 

To help your object users along, a common practice is to create a typedef for your interfaces that automatically accounts for the pointer's necessity. Many of the core COM interfaces (IUnknown, IDispatch, IClassFactory, and so on) include such a typedef, and you may wish to provide your own as well. Thus, given the existing IDraw interface definition, you may add the following typedef:

// MyInterfaces.h typedef LPDRAW IDraw*;

The object user can then make use of this when working with your objects:

// Object user code. #include "myinterfaces.h" LPDRAW pDraw;      pDraw = GetIDrawFromTriangle(); pDraw->Draw(); 

Interfaces Define a Contract

If you have not noticed by now, interfaces add a level of indirection to the programming world. Object users drive around an object through a vTable pointer, and have the power to treat all objects supporting a given interface the same way using interface-based polymorphism. By using some API level support, we have isolated the object user from the activation details of the given object as well, providing a deeper level of encapsulation. In order for users to work with objects in this way, we need a set of rules to abide by.

As you read more and more about interface-based programming you are bound to run into the term contract. This sounds much more corporate than it really is. A contract in real-world terms involves at least two parties who make a solid and binding agreement. In interface-based programming, the contract revolves around the object user (henceforth referred to as the client) and the object itself (often called the coclass in COM speak).

The interface contract is very clear. The object itself agrees to the following:

"Once an object supports a given interface, the object in question must always continue to support the interface without modification (e.g., you can't go changing the names or parameter sets of the methods, you can't reorder the methods, and you certainly can't rename the interface itself)."

The client agrees to the following terms in the same contract:

"I, as a client, will abide by the calling conventions as set forth by the interfaces and methods contained within them."

By far, the most weight in this contract is placed on the object. First, an object needs to be quite certain the interface it is going to support will suit its needs, as once the interface is supported, there is no going back.



 < 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