6.8 Interface Sharing


Interface sharing is the definition of methods in a base class that are guaranteed to be available in all derived classes, even if these methods are not implemented in the base class. Not requiring that the methods be implemented in the base class distinguishes interface sharing from type casting. Methods that are guaranteed to be present in all derived classes define a shared interface in the sense that all objects of derived classes can be manipulated through this common interface, even if the user of the object is not aware of the object's exact type. Methods defined in the base class, of course, are part of this shared interface. However, a guarantee can be given in a base class even when the method is implemented only in the derived classes. In this way, interface sharing is similar to type casting, but it is more powerful because it allows derived class methods to be used in addition to base class methods.

A revised version of the Clock class, the relevant code of which is shown below, is an example of a class in which interface sharing is needed. From the point of view of a Clock object, the Number and Cycler classes must be treated as two distinct classes. Despite the code sharing in the Number and Cycler classes, their types are not similar enough for them to be treated interchangeably or uniformly by the Clock class, which implies that the Clock class must have distinct data members and overloaded methods to interact with both Number and Cycler objects.


Revised Clock Class
  class Clock {
    private:
        Number*  number;        // Connect(ed)To a Number
        Cycler*  cycler;        // Connect(ed)To a Cycler   
        ...
    public:
        ...
        ConnectTo(Number& cnt);
        ConnectTo(Cycler& cnt);
        ...
   };
   void Clock::ConnectTo(Number& cnt) { number = &cnt;);
   void Clock::ConnectTo(Cycler& cnt) { cycler = &cnt;);
   void Clock::Notify() { if (number) number->Next(); else
                          if (cycler) cycler->Next();   }

Not only are the Number and Cycler classes distinct types, but all of the many subclasses of DisplayableNumber are distinct. To deal with all of these subclasses, the Clock class would need numerous pointer variables, numerous overloaded ConnectTo methods, and repeated code in the Notify method, as shown in the following figure.


Repetitive Code in the Clock Class
  class Clock {
    private:
        Number*         number;         // Connect(ed)To a Counter   
        Cycler*         cycler;         // Connect(ed)To a Cycler   
        BiCycler*       biCycler;       // Connect(ed)To a BiCycler
        UpCounter*      upCounter;      // Connect(ed)To a UpCounter     
        DownCounter*    downCounter;    // Connect(ed)To a DownCounter    
        BatchCounter*   batchCounter;   // Connect(ed)To a BatchCounter  
        JumpCounter*    jumpCounter;    // Connect(ed)To a JumpCounter  
        SwitchCounter*  switchCounter;  // Connect(ed)To a SwitchCounter  
        ...
    public:
        ...
        ConnectTo(Number&         cnt);
        ConnectTo(Cycler&         cnt);
        ConnectTo(BiCycler&       cnt);
        ConnectTo(UpCounter&      cnt);
        ConnectTo(DownCounter&    cnt);
        ConnectTo(BatchCounter&   cnt);
        ConnectTo(JumpCounter&    cnt);
        ConnectTo(SwitchCounter&  cnt);
        ...
   };
   void Clock::ConnectTo(Number&    cnt) { counter   = &cnt;};
   void Clock::ConnectTo(Cycler&    cnt) { cycler    = &cnt;};
   void Clock::ConnectTo(BiCycler&  cnt) { biCycler  = &cnt;};
   void Clock::ConnectTo(UpCounter& cnt) { upCounter = &cnt;};
   ... // rest of ConnectTo methods for other subclasses 
   void Clock::Notify() { if (number)    number->Next();    else
                          if (cycler)    cycler->Next();    else
                          if (biCycler)  biCycler->Next();  else
                          if (upCounter) upCounter->Next(); 
                          ... // similar tests for other subclasses   
                         }

When designing real system, such a situtation is not tolerable for three reasons. First of all, it leads to fragile software, becausewhenever a new subclass of DisplayableNumber is introduced, the Clock class must also be modified. Second, the code is large: the amount of code written and the amount of compiled object code grows as the number of subclasses increases. And third, the code is inefficient: the long if-then-else structure in the Notify method increases the execution time of the method as the number of subclasses increases.

For building more efficient and flexible systems, another technique must be used. The specific problem is that the Clock class uses a method (Next) defined in the various derived classes (Number, Cycler, etc.) but not defined in their base class (DisplayableNumber). As was seen with the NumberPanel, it is possible through type casting for the Clock class to use any of the methods defined in the base class even though the actual object is a Number or Cycler. Type casting will not suffice, however, when the method to be called is not in the class to which the actual object is cast. For example, type casting a Number object to a DisplayableNumber yields an interface that does not include the Next method.

Virtual Methods

The shared interface is defined by declaring one or more methods to be "virtual" or "pure virtual." For a virtual method, the base class provides a default definition of the method. In some cases, the default definition may be a do-nothing or null method - one that takes no actions. A pure virtual method is one in which the base class does not provide a default method definition. In this case, the responsibility for providing the required method implementation is passed to the derived class.

A virtual method in a base class allows an overriding derived class method to be called even if the caller is only aware of the base class type. In our example, the DisplayableNumber class would define a virtual Next method with an empty (null, do-nothing) implementation. Each derived class would then override the virtual method with the real method - the real method would provide the implementation that is appropriate for the derived class. A Clock object would then be able to call the Next method of a DisplayableNumber derived class object even if (and especially if) the exact type of the called object is unknown by the Clock object.

The DisplayableNumber class with a virtual Next method would be declared as shown below. The Number class, and all other subclasses of DisplayableNumber, remain unchanged. Each of these subclasses contains an overriding Next method. Note that the virtual keyword only appears in the definition of the class (i.e., when the method is defined in the .h file) and not in the implementation (i.e., where the code for the method is given).


Declaring a Virtual Method
class DisplayableNumber {
   private:
      ...
   public:
      ...
      virtual void Next();            // virtual Next method
      ...
};
// in implementation file
void DisplayableNumber::Next() {}       // null default definition

      

The Clock class can now be redeclared to take advantage of the shared interface. The important part of this redefinition is shown in the following figure, where it should be noted that only a single base class pointer is needed, the argument passed to ConnectTo can be either a pointer or a reference to a DisplayableNumber object, and the Notify method invokes the Next method uniformly for all types of DisplayableNumber objects. This solves the three problems noted above, because as the number of subclasses of DisplayableNumber increases, the Clock class remains unchanged; the repetitive source and object code is eliminated; and the execution time of the Notify method is constant.


Redefining the Clock Class
class Clock 
{
   private:
      DisplayableNumber *number;      // only know about base class
   public:
      void ConnectTo(DisplayableNumber* dn);
      void ConnectTo(DisplayableNumber& dn);
      void Notify();
};
// in implementation file
void Clock::ConnectTo (DisplayableNumber* dn) { number = dn; }
void Clock::ConnectTo (DisplayableNumber& dn) { number = &dn; }
void Clock::Notify() 
{  number->Next();         // invokes derived class method
   number->Show();         // invokes  base class method
}

       

The revised Clock and DisplayableNumber classes may be used as follows:

       
        Clock   oneMinute(60*1000), 
                oneSecond(1000);
        Number   minutes(0);
        Cycler  seconds(60);
        oneMinute.ConnectTo( (DisplayableNumber&) minutes);
        oneSecond.ConnectTo( (DisplayableNumber&) seconds);
        oneMinute.Start();
        oneSecond.Start();

In this instance two different derived class objects are passed as arguments to the ConnectTo method. In one case, the object is a Number object, and in the other, the object is a Cycler object. However, because of the shared interface defined by the virtual Next method, the Clock class is able to handle uniformly all objects derived from the DisplayableNumber class. Alternatively, the same effect can be accomplished using pointers as the following code shows:

       Clock   oneMinute(60*1000), 
                oneSecond(1000);
        Number  *minutes  = new Number(0);
        Cycler  *seconds  = new Cycler(60);
        oneMinute.ConnectTo( (DisplayableNumber*) minutes);
        oneSecond.ConnectTo( (DisplayableNumber*) seconds);
        oneMinute.Start();
        oneSecond.Start();

In this code the Number and Cycler objects are passed by pointer.

Pure Virtual Methods

In some cases, a default implementation cannot reasonably be given in a base class. It is, perhaps, questionable whether it is a good design decision to provide an empty Next method in the DisplayableNumber class, because providing a class with a Next method suggests that something will happen to advance the state of the object when the method is invoked. The fact that nothing happens may reasonably be viewed as counterintuitive or misleading. In other cases, it is clear that no reasonable default implementation can be given for a method. Consider, for example, the design of a base class that generalizes the concept of a geometric shape. While it is desirable to be able to require that each particular shape (Rectangle, Circle, etc.) be able to draw themselves via a Draw method or report their screen area by an Area method, there is clearly no default implementation of either of these methods that passes any reasonable standard.

A pure virtual method declares a method that must be implemented in a derived class because no implementation of the virtual method is given in the base class. A pure virtual method is used when the dynamic binding of a virtual method is desired, but no default implementation is given in the base class. A pure virtual method is a virtual method because the derived class method is used when the pure virtual method is invoked.

The syntax for declaring a pure virtual method is shown in the following redefinition of the DisplayableNumber class. To avoid confusion this new class is called an AbstractDisplayableNumber.

       class AbstractDisplayableNumber {
        ...
        public:
                virtual void Next() = 0;        // pure virtual method
        ...
        };

The additional syntax "= 0" is used to denote a pure virtual method. The intuition behind this syntax is somewhat strained - it may simply have to be committed to memory.

A class containing one or more pure virtual methods is referred to as an abstract base class. The word abstract is used to suggest that the class can describe a wide range of possible objects, depending on how the pure virtual methods are implemented by the derived classes. In contrast, the term concrete class is sometimes used to refer to a class that does not have pure virtual methods or has given implementations for all such methods. This class is concrete because it denotes a fixed, specific type of object. It is not possible to create objects of abstract classes. For example, the following code would not be allowed:

       AbstractDisplayableNumber adn(100);    // not allowed


This restriction enforces the guarantee that a pure virtual method will be implemented by a derived class. If a derived class does not implement all of the pure virtual methods that it inherits, then it is also considered an abstract class. Thus, objects with unimplemented pure virtual methods cannot be created. This restriction is reasonable because an abstract base class is incomplete, it does not provide the implementation of its pure virtual methods.

It is possible, and often done, to have pointers to abstract base classes. Such pointers are needed in order to be able to refer to a derived class object without knowing its exact type, as in the following code where a class is derived from AbstractDisplayableNumber:

       class ConcreteCounter : public AbstractDisplayableNumber {
        ...
        };
        void ConcreteCounter::Next() { ...some implementation ... }

The next examples illustrate what can and cannot be done with abstract classes and pointers to abstract classes.

  AbstractDisplayableNumber *adn;      // ok - pointer to abstract class
   ConcreteCounter           *cc;       // ok - pointer to concrete class
   AbstractDisplayableNumber n(100);    // no - class is abstract
   ConcreteCounter c(100);              // ok - instance of concrete class
   cc = &c;                             // ok - types are the same
   adn = &c;                            // ok 

The last line of code above ("adn = &c;") is meaningful because it allows a concrete object (i.e., an object of a concrete class) to be manipulated only by knowing its abstract base class.

Manipulating an object of a concrete class through a pointer to its abstract base class is both useful and safe. It is useful because it allows very flexible systems to be invented; the system can manipulate a wide variety of concrete objects knowing only their general (abstract class) properties. It is also safe to manipulate objects in this way because the pure virtual methods are guaranteed to be implemented in the concrete object - that is what makes them concrete. For example, the usage

  AbstractDisplayableNumber *adn;      
   ConcreteCounter c(100);              
   adn = &c;                            
   adn->Next();

is always safe because the Next method is guaranteed to be implemented in whatever concrete object is pointed to by the pointer adn.

Polymorphism

The term polymorphism is used to describe how objects of different classes can be manipulated uniformly. Through polymorphic techniques it is possible to write code (such as the Clock class above) that manipulates many different forms (subclasses of DisplayableNumber) of objects in a uniform and consistent manner without regard for their individual differences. The flexibility and generality of polymorphic structures is one of the significant advantages of object-oriented programming. Learning how to recognize opportunities to exploit this possibility takes practice.

The strategy for developing a polymorphic structure begins with identifying the common methods across a group of similar but not identical types of objects. A class hierarchy is then defined in which the common methods are placed in a base class, while the remaining methods are organized into classes derived from this base class. The interface of the base class defines the shared interface through which an object of any of the particular subclasses may be manipulated. It is important to remember that while methods of the shared interface must be declared in the base class, they may be left abstract with the subclasses assuming the responsibility for providing the definition (code) for the abstract methods.

The general class structure for polymorphism is shown in the figure below. The base class contains one or more virtual functions (vm1, vm2, vm3) that represent the shared interface through which the objects of the derived classes may be manipulated. The virtual functions may be given default implementations in the base class, or they may be defined as pure virtual functions. These virtual functions are defined, or overridden if necessary in the derived classes. If the base class methods are pure virtual ones, then the methods must be defined in derived classes from which objects are to be instantiated. If the base class methods are virtual, but not pure virtual, then the methods may be overridden in the derived classes. Objects can be instantiated from the derived class even if the virtual methods are not overriden.



Class Structure for Polymorphism

Several examples of polymorphism will be given. To stress the importance and generality of this concept, the examples are taken from class hierarchies that are implemented in different object-oriented programming languages. Only the high-level structure of the classes is presented so that the important point is not obscured by the differences in syntax among the various languages.

One example of polymorphism can be found in the Abstact Windowing Toolkit (AWT) class hierarchy. This class hierarchy is part of the Java run-time environment and the classes are written in Java. A portion of the AWT class hierarchy is shown below. The Component class is the base class for numerous derived classes, only some of which are shown in the following figure. This base class represents the shared interface of a range of different entities that can appear in a user interface built with Java and the AWT.



The Component Class Hierarchy

The Component class includes a method inside(int, int) that determines if the point defined by the two integer input parameters lies inside the given component. This method is useful, for example, in determining if the user has just clicked on a button - it can check if the current coordinates of the mouse are inside a button when a click event occurs. Exactly how a component checks for inside will depend on its nature and geometry. Nonetheless, it is meaningful to inquire of any component whether the coordinates are inside of that component.

Using the polymorphism enabled by the Component class, the essential pieces of a simple ComponentManager are shown below (rendered in C++). The critical element of this ComponentManager is its ability to manipulate any of the subclasses of Component. When the inside method of the ComponentManager is invoked, it in turn queries the Component objects that it knows about in search of one that contains the coordinates (x,y). If such a Component is found, then a pointer to that Component is returned. Otherwise, a null value is returned indicating that the point does not lie inside of the Components managed by the ComponentManager.


Use of Polymorphism: Component Example
  class ComponentManager {
     private:
        Component *member[10];
        int        number;
        ...
     public:
         ComponentManager() : number(0) {}
         void Enroll(Component *cmp) { member[number++] = cmp; }   
         Component* inside(int x, int y) {
                 for(int next = 0;  next<number; next++) 
                    if (member[next]->inside(x,y) )
                        return member[next];
                 return NULL;
         }
         ...
    };
   

Static and Dynamic Binding

The term dynamic binding describes the mechanics of virtual functions. "Binding" refers to the act of selecting which body of code will be executed in response to a function invocation. In languages like C, the binding is done completely at compile-time, even if overloaded methods are involved. Binding done at compile-time is also called "static binding," static in the sense that once set, it does not change. However, with virtual methods, the binding cannot be done completely at compile-time because the actual body of code to execute when a virtual function is called depends on the actual class of the object, something that is not known at compile-time. To see the need for binding at run-time, consider the Clock class defined above, a portion of this is repeated here:

       class Clock {
        private:
                DisplayableNumber *number;      // only know about base class
        public:
                void Notify();
        };
        void Clock::Notify() { 
                number->Next();         // invokes derived class method
                ...
        }

The binding for the invocation of the Next method in the Notify method cannot be done at compile-time because the compiler does not know the actual class of the object being pointed to by the pointer variable number. In fact, there is no single binding that will suffice as different Clock objects may be bound to different subclasses of DisplayableNumber. What the compiler does instead is to build an efficient way of performing the binding at run-time.

  1. Define and implement a TextDisplay base class that captures the common properties of the TextBox and Message classes. The intent of this base class is to exploit polymorphism so that other classes can manipulate an object of type TextDisplay without being aware of, or dependent upon, knowledge of the exact class of the object.

  2. Using the TextDisplay class, define and implement a class FlexCounter by revising the Number class. A FlexCounter can be connected to either a TextBox or a Message object. Be sure to exploit the polymorphic properties of the TextDisplay class.

  3. Using the TextDisplay class, define and implement a class FlexDisplayableNumber by revising the DisplayableNumber class. A FlexDisplayableNumber is defined so that its method ShowIn can accept either a TextBox object or a Message object as a parameter.

  4. Illustrate the flexibility properties gained through inheritance by finding the number of associations that can be built using the Clock class (as revised in this section), the TextDisplay, and the FlexDisplayableNumber.


  5. Define and implement a revised Button class named VirtualButton to serve as a base class for wide range of specific types of buttons. Include in the VirtualButton class a virtual method "void OnPush()" which defines an action that the button object should perform when its corresponding button on the screen is pushed. The default behavior for this method is to do nothing. A do-nothing button would be used as follows:

        VirtualButton vButton("Test", Location(20,20), Shape(60,20));
    					
    

    Frame window("ButtonTest", Location(100,100), Shape(200,200));

    Panel panel(window, Location(20,20), Shape(100,50));

    //...

    void OnPush(char* buttonName)

    { if vButton.isEqual(buttonName) vButton.OnPush();

    }

  6. Define and implement a revised Button class named AbstractButton that functions like a VirtualButton except that the OnPush method is declared to be a pure virtual method.


  7. Define and implement a class StartButton that is derived from VirtualButton. A StartButton object can be connected to a Clock object in an association. The OnPush method of the StartButton should call the Clock::Start method.

  8. Define and implement a class StopButton that is derived from VirtualButton. A StopButton object can be connected to a Clock object in an association. The OnPush method of the StopButton should call the Clock::Stop method.

 


     


©1998 Prentice-Hall, Inc.
A Simon & Schuster Company
Upper Saddle River, New Jersey 07458

Legal Statement

 

ÿ