Interfaces

Interfaces nennt man Klassen, die ausschließlich aus virtuellen Funktionen bestehen und zwar ohne, dass es eine Implementierung gibt. Das bedeutet, dass diese Klasse soweit überhaupt nicht benutzbar ist.

pure virtual Methods

Methoden, die nicht implementiert werden, werden in C++ 'pure virtual' genannt. Andere Sprachen benutzen ein eigenes Schlüsselwort, häufig 'abstract', entsprechend spricht man auch von abstrakten Methoden.

Eine 'pure virtual' Methode wird fast wie eine virtuelle Methode erstellt, sie wird lediglich zusätzlich mit 0 initialisiert:

class Encoder
{
  virtual bool Encode( char const * source, char * destination, int length ) = 0;
  virtual bool Decode( char const * source, char * destination, int length ) = 0;
};

Die Klasse Encoder kann nun nicht mehr instanziiert werden, schließlich könnte man sonst die Funktion Encode nicht rufen, da sie bisher nicht implementiert wurde. Es gibt bisher ja nur die Behauptung, dass es eine Methode geben soll.

Eine pure virtual Method muss also in einer Ableitung überschrieben werden.

Überschreiben von pure virtual Methods

Eine Klasse Encoder lässt sich nicht erstellen, bevor die beiden pure virtual Methods implementiert haben. Eine Ableitung muss also die Methoden implementieren oder bleibt eine abstrakte Klasse, die ebenfalls nicht instanziierbar ist. Das ist vollkommen in Ordnung, so lassen sich für eine weitere abstrakte Basisklasse weitere Methoden hinzufügen - abstrakte, wie auch implementierte.

Nehmen wir nun an, dass wir nun eine Klasse implementieren wollen, die das Interface Encoder implementieren soll:

class CaesarChiffre : public Encoder
{
  bool Encode( char const * source, char * destination, int length );
  bool Decode( char const * source, char * destination, int length );
};
 
bool CaesarChiffre::Encode( char const * source, char * destination, int length )
{
  int i;
  for( i = 0; i < length && source[ i ]; i++ )
  { 
    destination[i] = source[i];
 
    if( source[i] >= 'A' && source[i] <= 'Z' )
      if( destination[i] > 'C' ) destination[i] -=3;
      else                       destination[i] +=23; 
    else
      if( source[i] >= 'a' && source[i] <= 'z' )
        if( destination[i] > 'c' ) destination[i] -=3;
        else                       destination[i] +=23; 
  }
 
  if( i < length )
    destination[i++] = '\0';  
 
  return i <= length; 
}
 
bool CaesarChiffre::Decode( char const * source, char * destination, int length )
{
  int i;
  for( i = 0; i < length && source[ i ]; i++ )
  { 
    destination[i] = source[i];
 
    if( source[i] >= 'A' && source[i] <= 'Z' )
      if( destination[i] < 'X' ) destination[i] +=3;
      else                       destination[i] -=23; 
    else
      if( source[i] >= 'a' && source[i] <= 'z' )
        if( destination[i] < 'x' ) destination[i] +=3;
        else                       destination[i] -=23; 
  }
 
  if( i < length )
    destination[i++] = '\0';  
 
  return i <= length; 
}

Die Bedeutung des Interface

Wir haben nun eine Schnittstellenbeschreibung (Interface) namens Encoder, die Objekte kennzeichnet, wie man Texte kodiert und wieder dekodiert. Ein solches Objekt verfügt auf jeden Fall über die Methoden Encode und Decode.

Das Interface wird in der Klasse CaesarChiffre implementiert. Das Interface selbst besitzt keine Implementation und keine Variablen. Der Unterschied zur normalen Ableitung mit virtuellen Methoden liegt lediglich darin, dass ein Interface ein Sonderfall ist: es besitzt überhaupt keine Implementation.

Warum wird es dann besonders behandelt?

Klassen, die keine Variablen als Member haben, können auch nicht das Diamant-Problem entwickeln. Sprachen wie C# oder Java bauen darauf, dass sie so das Diamant-Problem ausklammern können.

Wichtig dabei ist, dass Interfaces keine Variablen untergeschoben bekommen, die mit Hilfe einer Referenz auf eine Variable, die durch eine Get…-Methode zu erreichen ist. Das ist verlockend, um einem Objekt eine Konfiguration unterzujubeln, die dann über das Interface erreichbar wäre. Implementiert man ein solches Interface mehrfach, so gibt es auch mehrere Konfigurationen und schon haben wir mit dem Interface wieder die gleichen Probleme wie bei der Mehrfachvererbung: das Diamant-Problem.

Für ein Interface gilt es also darauf zu achten, dass nur Funktionen verwendet werden, die vollständig über die Parameter konfiguriert werden und die ihre Konfiguration nicht selbst halten. Müssen Objekte Daten halten, so handelt es sich um Mehrfachvererbung.