ITworld.com
  Search  
ITworld Home Page ITworld Webcasts ITworld White Papers ITworld Newsletters ITworld News ITworld Topics Careers ITworld Voices ITwhirled Changing the way you view IT
 

Repeated interface implementations in Java, C++, and Delphi

ITworld.com 1/4/01

A language's lack of direct support for certain constructs need not constrain your software designs

Michael L. Perry, ITworld.com

In recent columns, I've discussed the rule of interface: what it's good for, how to implement it, and what problems can arise when it's not strictly followed. While the rule states that a class implements interfaces, it does not restrict the type or number of those interfaces. Many languages, however, do restrict the ways that classes can implement interfaces. When you use such languages, you may have to do some creative coding to achieve your desired results.

Case in point: It is sometimes desirable for an object to provide multiple implementations of a single interface. Yet none of the three languages I routinely examine in this column -- Java, C++, and Delphi -- directly supports repeated interface implementation.

This won't, however, stop a creative coder from doing his or her job. Suppose, for example, we want to write an application that displays a contact list, which it reads from a comma-delimited text file. We can design our program as two separate classes: one that parses comma-separated value (CSV) files in general, and one that records the application-specific contact list.

apd-perry-0104
Structure of contact application

As the parser consumes the raw data file, it passes each value to the contact list via the ICSVColumn interface. Each column in the contact list maps to a different piece of contact information, so each is associated with a different instance of the ICSVColumn interface. The structure is represented in the figure on the right.

Satisfied that our design correctly obeys the rule of interface, we choose our language and move on to code.

Java and adapter classes
Java offers direct interface support through the interface keyword, which we'll use to write the ICSVColumn interface. That support is limited, though; it provides only one way for a class to directly implement an interface.

This limitation is a byproduct of the way Java overrides methods: If a class method's name and parameter signatures match those of a base class's or interface's method, the class method will override the base's method. Since overrides are determined entirely by name and parameter signature, a class cannot directly provide more than one implementation. A class must implement an interface indirectly to implement it repeatedly.

To work around this limitation, use a group of adapter classes. An adapter class, like the similarly named pattern from the GOF (Design Patterns, by Gamma et al., aka "The Gang of Four"), implements a well-known interface, delegating its implementation to an adaptee. The biggest difference between a group of adapter classes and the GOF adapter pattern is intent. The GOF pattern is used to convert one interface to another; a group of adapter classes is used to implement a single interface repeatedly.

In the code that follows, the ContactList class provides repeated implementations of the ICSVColumn interface via anonymous adapter classes.


interface ICSVColumn
{
  public void onRead( String value );
}

class CSVParser
{
  public void addColumn(
    ICSVColumn column )
  {
    // Add the column interface to
    // the list.
  }

  public void readFile(
    String fileName )
  {
    // As the file is read,
    // call each interface
    // in order.
  }
}

public class ContactList
{
  public void onReadFirstName(
    String value ) {}
  public void onReadLastName(
    String value ) {}
  public void onReadPhoneNum(
    String value ) {}
  public void onReadEmail(
    String value ) {}

  public void loadContacts(
    String fileName )
  {
    CSVParser parser = new CSVParser();
    parser.addColumn( new ICSVColumn() {
      public void onRead( String value ) {
        onReadFirstName( value ); } } );
    parser.addColumn( new ICSVColumn() {
      public void onRead( String value ) {
        onReadLastName( value ); } } );
    parser.addColumn( new ICSVColumn() {
      public void onRead( String value ) {
        onReadPhoneNum( value ); } } );
    parser.addColumn( new ICSVColumn() {
      public void onRead( String value ) {
        onReadEmail( value ); } } );

    parser.readFile( fileName );
  }
}

Java programmers may recognize that the AWT (Abstract Windowing Toolkit) uses this technique for event handling. The AWT uses adapter classes for the same purpose. One class implements a single interface repeatedly. In the case of the AWT, the interface would be one of the EventListener derivatives; the class would be an application-defined Container derivative. Such a container is designed to respond to the same type of events fired from different components.

C++ and interface interpreters
Unlike Java, C++ makes no distinction between interfaces and classes. A C++ interface is really an abstract base class, which, like all C++ classes, must use multiple inheritance to directly implement multiple interfaces. Multiple inheritance is fraught with danger and must be used with great care. Scott Meyers enumerates the caveats of multiple inheritance in Effective C++; refer to item 43. C++ and Java use the same name-matching mechanism to resolve method overrides. Therefore, as with Java, we cannot use multiple inheritance to directly implement an interface repeatedly.

Again, however, we can overcome this limitation. As Meyers illustrates (also in item 43), C++ provides a mechanism that safely uses multiple inheritance to effectively "rename" virtual functions. When we apply this mechanism to the problem of repeated interface implementation, we create classes that I like to call interface interpreters.

Like an adapter, an interface interpreter implements an interface by delegation. The difference is the identity of the object that implements the interface. Each adapter class defines a separate object; each interface interpreter defines a new pure virtual function for one object. In essence, this new function provides one interpretation of the interface -- hence the name. In the code below, the ContactList class uses multiple inheritance of interface interpreters to repeatedly implement the ICSVColumn interface:


class ICSVColumn
{
public:
  virtual void OnRead(
    LPCTSTR strValue ) = 0;
};

class CSVParser
{
public:
  void AddColumn( ICSVColumn *pColumn );
  void ReadFile( LPCTSTR strFileName );
};

class ICSVColumnFirstName: public ICSVColumn
{
public:
  void OnRead( LPCTSTR strValue )
    { OnReadFirstName( strValue ); }
  virtual void OnReadFirstName(
    LPCTSTR strValue ) = 0;
};

class ICSVColumnLastName: public ICSVColumn
{
public:
  void OnRead( LPCTSTR strValue )
    { OnReadLastName( strValue ); }
  virtual void OnReadLastName(
    LPCTSTR strValue ) = 0;
};

class ICSVColumnPhoneNum: public ICSVColumn
{
public:
  void OnRead( LPCTSTR strValue )
    { OnReadPhoneNum( strValue ); }
  virtual void OnReadPhoneNum(
    LPCTSTR strValue ) = 0;
};

class ICSVColumnEmail: public ICSVColumn
{
public:
  void OnRead( LPCTSTR strValue )
    { OnReadEmail( strValue ); }
  virtual void OnReadEmail(
    LPCTSTR strValue ) = 0;
};

class ContactList :
  ICSVColumnFirstName,
  ICSVColumnLastName,
  ICSVColumnPhoneNum,
  ICSVColumnEmail
{
public:
  void OnReadFirstName( LPCTSTR strValue );
  void OnReadLastName( LPCTSTR strValue );
  void OnReadPhoneNum( LPCTSTR strValue );
  void OnReadEmail( LPCTSTR strValue );

  void LoadContacts( LPCTSTR strFileName )
  {
    CSVParser parser;

    parser.AddColumn(
      (ICSVColumnFirstName *)this );
    parser.AddColumn(
      (ICSVColumnLastName *)this );
    parser.AddColumn(
      (ICSVColumnPhoneNum *)this );
    parser.AddColumn(
      (ICSVColumnEmail *)this );

    parser.ReadFile( strFileName );
  }
};

Delphi and procedural types
Like Java, Delphi treats interfaces and classes differently. As you might expect, adapter classes that work well in Java will work well in Delphi. Unfortunately, Delphi does not offer the convenience of anonymous class declaration, so similar code is a bit bulkier. However, Delphi also offers a completely different kind of support for repeated interface implementation. If the interface in question has only one method, Delphi allows you to declare that method as a procedural type.

Because ICSVColumn has only the OnRead method, we need not declare it with the interface or class keyword. Instead, we can simply declare it a procedure of object.

When declaring the procedural type, we must declare the parameters that the method expects without giving the method a name. Because procedural types have no method names, the implementing class may assign the method any name.

Furthermore, a single class may provide multiple interface implementations for the same procedural type. The procedural type still conforms to the rule of interface -- it identifies the object, defines a protocol, and hides the implementation -- but does so completely differently than adapter classes or interface interpreters.

Observe how the following code uses procedural types to achieve repeated interface implementation:


type
  TCSVColumn = procedure( strValue: string ) of object;

  TCSVParser = class(TObject)
  public
    constructor Create;

    procedure AddColumn( column: TCSVColumn );
    procedure ReadFile( strFileName: string );
  end;

  TContactList = class(TObject)
  public
    procedure OnReadFirstName( strValue: string );
    procedure OnReadLastName( strValue: string );
    procedure OnReadPhoneNum( strValue: string );
    procedure OnReadEmail( strValue: string );

    procedure LoadContacts( strFileName: string );
  end;

procedure TContactList.LoadContacts(strFileName: string);
var
  parser: TCSVParser;
begin
  parser := TCSVParser.Create;

  parser.AddColumn( OnReadFirstName );
  parser.AddColumn( OnReadLastName );
  parser.AddColumn( OnReadPhoneNum );
  parser.AddColumn( OnReadEmail );

  parser.ReadFile( strFileName );
end;

Java, C++, and Delphi each have their own mechanism for repeated interface implementation. Since none can claim to be the best solution, however, you can base your language selection on other factors. You won't need to select a target language until after you've designed your software and identified those other factors. Software designs that follow the four rules of modeling can be implemented in almost any modern object-oriented language.

Michael L. Perry has been a professional Windows developer for over six years and maintains expertise in COM+, Java, XML, and other technologies currently shaping the programming landscape. He formed Mallard Software Designs in 1998, where he applies the mathematical rigor of proof -- establishing the correctness of a solution before implementing it -- to software design. He is the moderator of ITworld.com's Windows Application Development discussion.





 
www.itworld.com    open.itworld.com     security.itworld.com     smallbusiness.itworld.com
storage.itworld.com     utilitycomputing.itworld.com     wireless.itworld.com

 
Contact Us   About Us   Privacy Policy    Terms of Service   Reprints  

CIO   Computerworld   CSO   GamePro   Games.net   Industry Standard   Infoworld   ITworld  
JavaWorld   LinuxWorld  MacUser   Macworld   Network World   PC World   Playlist  

DEMO   IDG Connect   IDG Knowledge Hub   IDG TechNetwork   IDG World Expo  

Copyright © Computerworld, Inc. All rights reserved

Reproduction in whole or in part in any form or medium without express written permission of Computerworld Inc. is prohibited. Computerworld and Computerworld.com and the respective logos are trademarks of International Data Group Inc.