Fitting in with multiple frameworks in C++ and Java

Fitting in with multiple frameworks in C++ and Java

By Alan Griffiths

Overload, 7(31):, April 1999


Abstract

In an application that is using more than one framework there are occasions when it is desirable for a single object to perform a role in two (or more) frameworks. We investigate two design alternatives for combining default implementations provided by the frameworks with a user defined class that interacts with both.

Outline implementation for these designs are given C++ and Java. As a result we reach the surprising conclusion that in changing from one implementation language to the other it is correct to change between these design alternatives.

Introduction

The idea for this article came from a discussion with a colleague working in Java about the difficulties he was having trying to combine his extensions to a user interface framework (in this case Sun's Swing) with the need to integrate these extensions with a framework that provides an undo facility.

Although I've been dabbling with Java for over two years I still tend to "think in C++" and translate my C++ approach into Java. At the design level this is usually an effective way to apply the experience I've gained over the last decade - it would be surprising if a design stopped working just because it is expressed in a different language. (Of course there are some pitfalls to avoid since C++ supports paradigms that Java doesn't - most notably generic programming.)

My initial suggestion was based on the way I've tackled similar problems in C++, by using the "mixin" idiom to interface to both frameworks and is shown below. Subsequently, I became dissatisfied with using this approach in Java. Upon examining Java code I'd written, I realised that in analogous situations I was using a different approach based on the "adapter pattern". This seems more natural in Java and is also illustrated below.

To make the following discussion concrete, let us assume that we are using two frameworks; one framework delivers the user interface, whilst the second is an engine for a board game such as chess. In writing a program we reach the conclusion that we both want to add a piece to a display in the user interface and to place it in the board. To achieve this we require a class that that implements both user interface behaviour (displaying itself) and application domain behaviour (moving on the board).

Before presenting the code I'd like to make two points:

  1. in the general case framework interfaces will have a number of methods, but for the sake of brevity I show only one, and

  2. in presenting these approaches in both languages I have adopted a coding style that emphasises the similarities of design but is not idiomatic in either language (e.g. it unusual to implement C++ methods in the class body).

Application Frameworks, Mixins and Adapters

An Application Framework is a library of interacting classes that provides both the design and a skeleton implementation of some generic subsystem (for instance a user interface). By modifying the state of some classes (e.g. adding buttons) and specialising the behaviour of others (providing a meaningful response to a button push) the framework is adapted to the needs of the current program.

A mixin class is one that defines a protocol required for an operation (such as printing). Inheriting from a mixin class is a declaration that the protocol is supported (i.e. that the class is printable) rather than the more common "is a" relationship. The use of mixins presupposes multiple inheritance (a document is a file, it is also printable).

An adapter class[ GoF ] is one used to translate between the interface required by some function (printing again) into that supplied by a class that doesn't implement that interface directly but does do something sufficiently similar (provision of display text). An adapter accepts messages from the function and converts them to corresponding ones understood by the target class.

The framework classes

Before looking at the different approaches to implementing our class we need to provide definitions in C++ and Java of the framework classes with which we will be working:

DisplayInterface

This defines the protocol used by the display framework to present an object on the screen.

Display

The part of the user interface that we wish to use to display our piece.

DefaultDisplayInterface

Provides a default, do nothing, implementation of the interface.

PieceInterface

This defines the protocol used by the game framework to manage pieces.

Board

The part of the game framework to which we wish to supply our piece.

DefaultPieceInterface

Provides a default, do nothing, implementation of the interface.

The first listings 1 and 2 provide corresponding C++ and Java code, so far the differences between the languages are minor and relate only to their syntax.

Listing 1: Framework classes in C++

class DrawingSurface;

class DisplayInterface {
public:
  virtual const DisplayInterface& 
  drawOn(DrawingSurface& surface) const = 0;
};

class Display {
public:
  void add(DisplayInterface*);
};

class DefaultDisplayInterface :
  public DisplayInterface {
public:
  virtual const DisplayInterface& 
  drawOn(DrawingSurface&) const
  { return *this; }
};

class PieceInterface {
public:
  virtual PieceInterface& moveTo(
   int file, int rank) = 0;
};

class Board {
public:
  void add(PieceInterface*);
};

class DefaultPieceInterface :
  public PieceInterface {
public:
  virtual PieceInterface& moveTo(int, int) 
  { return *this; }
};

Listing 2: Framework classes in Java

public interface DisplayInterface {
  public DisplayInterface 
  drawOn(java.awt.Graphics surface);
}

public interface Display {
  public void add(DisplayInterface d);
}

public class DefaultDisplayInterface
  implements DisplayInterface {
  public DisplayInterface 
  drawOn(java.awt.Graphics surface)
  {
    return this;
  }
}

public interface PieceInterface {
  public PieceInterface
  moveTo(int file, int rank);
}

public class Board {
  public void add(PieceInterface p);
}

public class DefaultPieceInterface
  implements PieceInterface {
  public PieceInterface
  moveTo(int file, int rank) { return this; }
}

First approach: using the default implementations as mixins

In C++ default implementations from several frameworks can be used directly: As can be seen from listing 3, if the default behaviour is all that is required then there is almost nothing to it! (However, this example will be revisited below under "dealing with clashes".)

Listing 3: First approach in C++

class DisplayPiece : 
  public DefaultPieceInterface, 
  public DefaultDisplayInterface {
};

In Java whilst we are able to provide the default implementations, the rules governing inheritance require that we inherit from at most one of them. (Java distinguishes "interfaces" which have no implementation whatsoever from other classes - one may inherit as many interfaces as one likes and implement the required methods, but at most one "ordinary" class.)

As usual an extra layer of indirection solves the problem - listing 4 introduces an extra class whose sole purpose is to delegate the default behaviour for both interfaces whilst allowing derived classes to override those that are of interest.

Listing 4: First approach in Java

// Default implementation for both frameworks
public class AbstractDisplayPieceInterface 
  implements DisplayInterface,
             PieceInterface {

  public DisplayInterface 
  drawOn(java.awt.Graphics surface) {
    d.drawOn(surface);
    return this;
  }

  public PieceInterface
  moveTo(int file, int rank) {
    p.moveTo(file, rank);
    return this;
  }

  final private DisplayInterface d
    = new DefaultDisplayInterface();
  final private PieceInterface p
    = new DefaultPieceInterface();
}

public class DisplayPiece
  extends AbstractDisplayPieceInterface {
};

Although this code proves that default interfaces can be mixed in Java I'm far from convinced that this is a good approach! There is a serious potential "gotcha" - when there are several methods defined in the interface it is possible for one of the default method implementations to be defined in terms of another - the approach shown doesn't work when only the latter method is overridden.

Second approach: deriving adapters from the default implementations

What I came to realised was that Java's "anonymous local classes" provide a very elegant alternative based on the adapter pattern described by the "Gang of Four"[ GoF ] (and that I was already using it on a regular basis).

The use of adapters frees the DisplayPiece class from the need to implement the interfaces required by the framework directly. Also an adapter for one framework doesn't need to implement any of the functionality required by the other, so multiple inheritance isn't an issue.

Listing 5 shows an implementation using this approach - the DisplayPiece class implements functionality corresponding to the methods we wish to adapt and two fragments of "client code" that create suitable adapters for the two frameworks.

Listing 5: Second approach in Java

public class DisplayPiece {
  private void
  myDrawOn(java.awt.Graphics surface) {}
  private void
  myMoveTo(int file, int rank) {}
}

// "client code" fragment #1
  void addToDisplay(Display d,
                    final DisplayPiece p) {
    d.add( new DefaultDisplayInterface() {
        public DisplayInterface 
        drawOn(java.awt.Graphics surface) {
          p.myDrawOn(surface);
          return this;
        } } );
  }

// "client code" fragment #2
  void addToBoard(Board g,
                  final DisplayPiece p) {
    g.add( new DefaultPieceInterface() {
        public PieceInterface
        moveTo(int file, int rank) {
          p.myMoveTo(file, rank);
          return this;
        } } );
  }

The reason for presenting the Java code first in this approach is that in C++ we still have a major design decision to make - something needs to own the adapter classes and to control their lifetimes - they have to be deleted when they are no longer used. (In Java the "virtual machine" reclaims memory that is no longer in use, C++ requires the programmer to know when this happens and to release it herself.)

For the sake of simplicity in listing 6 I've chosen to embed the adapters as nested classes within the DisplayPiece class. This ensures that they will be deleted with it. Another valid solution is to embed them into a separate controlling class.

Listing 6: Second approach in C++

class DisplayPiece {
public:
  DisplayPiece() : pia(*this), uia(*this) { }
  operator PieceInterface&() { return pia; }
  operator DisplayInterface&() { return uia; }
  void myMoveTo(int, int)
  { /* … … */ }
  void myDrawOn(DrawingSurface&) const
  { /* … … */ }

private:
  class PieceInterfaceAdapter :
    public DefaultPieceInterface {
  public:
    PieceInterfaceAdapter(
      DisplayPiece& adaptee) 
    : body(adaptee) { }
    PieceInterface& moveTo(int file, int rank)
    { 
      body.myMoveTo(file, rank); 
      return *this;
    }
    DisplayPiece& body;
  private:
    PieceInterfaceAdapter(
      const PieceInterfaceAdapter&);
    PieceInterfaceAdapter& operator=(
      const PieceInterfaceAdapter&);
  } pia;

  class DisplayInterfaceAdapter :
    public DefaultDisplayInterface {
  public:
    DisplayInterfaceAdapter(
      DisplayPiece& adaptee)
    : body(adaptee) { }
    const DisplayInterface& 
    drawOn(DrawingSurface& s) const
    { 
      body.myDrawOn(s); 
      return *this;
    }
    DisplayPiece& body;
  private:
    DisplayInterfaceAdapter(
      const DisplayInterfaceAdapter&);
    DisplayInterfaceAdapter& operator=(
      const DisplayInterfaceAdapter&);
  } uia;

  DisplayPiece(const DisplayPiece&);
  DisplayPiece& operator=(
    const DisplayPiece&);
};

In the first approach we found the Java code to be more elaborate than the C++, now that situation has been reversed. By making the C++ framework more complex and providing a template adapter class we could perhaps reduce the complexity of the client code a little, but the requirement for explicit memory management remains.

Dealing with clashes

Returning to the first approach, there is a possibility that both interfaces may have a method with the same signature. For the sake of argument suppose that both interfaces declare a method void clash() . If we were to override this method in our DisplayPiece class then we would have difficulty determining through which interface we were being invoked.

In C++ this issue can be resolved by adding a pair of intermediate classes into the hierarchy that effectively rename the message so that we can determine its origin. Listing 7 shows the way.

Listing 7: Dealing with clashes in C++

class IntermediateDisplay :
  public DefaultDisplayInterface {
public:
 // DO NOT OVERRIDE clash() -
 // override displayClash() instead
 virtual void clash() { displayClash(); }
 virtual void displayClash() { 
  DefaultDisplayInterface::clash();
 }
};

class IntermediatePiece :
 public DefaultPieceInterface {
public:
 // DO NOT OVERRIDE clash() -
 // override pieceClash() instead
 virtual void clash() { pieceClash(); }

 virtual void pieceClash() {  
  DefaultPieceInterface::clash();
 }
};

class DisplayPiece : 
 public IntermediatePiece, 
 public IntermediateDisplay {
};

As this technique relies on multiple inheritance there is no corresponding solution in the Java case.

Final thoughts

In a very real sense we have been looking at two different formulations of a single idea - an object that can interact with both the display and the board. What is surprising is that the choice of implementation language has such a dramatic effect on the ease with which each approach can be expressed. It could be argued that the correct translation of listing 3 is not listing 4 (which is not idiomatic Java), but listing 5 (which is).

The idea that an "adapter" is the correct translation of a "mixin" seems strange at first but Kevlin Henney [ Henney ] has previously observed similar relationship between "iteration" and "enumeration" when moving between languages. This change of form as a result of translation isn't just a characteristic of computer languages - Douglas Hofstadter [ Hofstadter ] dedicates a substantial volume to similar interactions between the ideas being expressed and the forms of expression in different natural languages.

Why does changing the implementation language change the way in which the problem is solved?

  1. The mixin approach works well in C++ because it removes the problem of object lifetimes that occurs in the adapter approach. In addition, the more flexible C++ inheritance model allows default behaviours to be combined and the occasional problem of methods clashing to be resolved.

  2. The adapter approach works well in Java both because of the lightweight syntax for declaring anonymous local classes and because the JVM, not the programmer has responsibility for controlling object lifetimes.

Mixins and adapters are both idioms I've used before, but I hadn't previously considered the possibility of a relationship between them. I see now that they are very close and solve related problems. The choice between them may depend on the fine detail of the problem at hand.

For those that wish to draw conclusions about the superiority of one language or the other I've shown that both choices are always available in C++, whilst the mixin approach may sometimes be problematic in Java. On the other hand the adapter approach is sufficient in Java and very flexible.

References

[GoF] ISBN 0-201-63361-2 " Design Patterns ", Gamma, Helm, Johnson, Vlissides, Addison-Wesley 1997

[Henney] " Idioms - Breaking the Language Barrier ", Kevlin Henney, European C and C++ Users Conference 1998

[Hofstadter] ISBN 0 7475 3349 0 " Le Ton beau de Marot " Douglas Hofstadter, Bloomsbury 1997






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.