Montag, 12. Mai 2014

Interfaces und abstrakte Klassen - Vergleich einiger Programmiersprachen

Interfaces und abstrakte Klassen sind in vielen Programmiersprachen das zentrale Ausdrucksmittel zur Deklaration abstrakter Typen. Im Blogartikel Abstraktheit - abstrakte Klassen und Interfaces wurden bereits die folgenden wesentlichen Unterschiede abstrakter gegenüber konkreten Typen herausgearbeitet:
  • Unvollständige Implementierung
  • Erforderliche Implementierung
  • Verhinderung der Erzeugung von Instanzen
  • Entkopplung durch Dependency Inversion
Im vorliegenden Artikel möchte ich nun die Realisierung und Nutzung von Interfaces und abstrakten Klassen in verschiedenen Programmiersprachen vergleichen. Die Darstellung gibt zudem eine Übersicht der historischen Entwicklung dieser Sprachkonzepte. Damit soll eine Grundlage für die Behandlung einiger Interface-bezogener Prinzipien, wie z. B. "Program to an Interface, not to an implementation" oder "Design by Contract" geschaffen werden.

Simula67 (1967)

Simula67 gilt als erste objektorientierte Programmiersprache, verfügt jedoch weder über abstrakte Klassen noch über Interfaces. Simula67 kennt virtual methods, d. h. überschreibbare Methoden, nicht aber pure virtual methods, die eine Klasse abstrakt und damit nicht instantiierbar machen. Das Schlüsselwort virtual wurde erstmalig mit Simula eingeführt und in Common Base Language [Dahl1970] wie folgt beschrieben:
Alle Klassen in Simula67 sind instantiierbar.

Smalltalk (1972)

Smalltalk nennt die Menge der Methoden, die für ein gegebenes Objekt aufgerufen werden kann, message protocol. Im Englischen Sprachraum wird daher auch gelegentlich der Begriff Protocol synonym zu Interface verwendet. Abstrakte Klassen oder Interfaces existieren in Smalltalk nicht. Aus der Sicht der Klienten wäre deren Verwendung ohnehin nicht möglich, da in Smalltalk Variablen nicht mit Typen deklariert werden können. Wie in Simula67 sind somit in Smalltalk alle Klassen instantiierbar.

C++ (1983)

C++ kennt abstrakte Klassen, aber keine Interfaces. In C++ wird eine Klasse abstrakt, d. h. nicht instantiierbar, indem sie eine pure virtual method deklariert. Eine solche Methode muss erstens mittels des Schlüsselwortes virtual deklariert sein, wodurch ihre Überschreibbarkeit ermöglicht wird. (C++ ist statically bound by default). Zweitens wird ihr ein leerer Funktionskörper zugewiesen, indem der Methodensignatur " = 0" angehängt wird. Eine solche Deklaration sieht dann wie folgt aus:

virtual void MyPureVirtualMethod() = 0;

Eine konkrete Subklasse muss für diese Methode einen Methodenkörper implementieren. Tut sie das nicht, so wird die Subklasse automatisch wieder abstrakt und somit nicht instantiierbar. (In sehr frühen Compiler-Versionen war es erforderlich, die Deklaration der Methode zu wiederholen, entweder als pure virtual method, womit die Klasse wieder abstrakt wurde, oder als Implementierung, was sie instantiierbar machte.)

Das "fehlende" Schlüsselwort abstract zur Kennzeichnung abstrakter Klassen wird meist so rechtfertigt, dass abstrakte Klassen per Definition unvollständig implementiert sein sollten. Um dies festzustellen, reicht das Vorhandensein einer pure virtual method aus. Das Schlüsselwort abstract wäre in diesem Sinne redundant. Die Kennzeichnung einer Klasse als abstract, obwohl sie keine pure virtual method enthält, wird als Verstoß gegen die geforderte Unvollständigkeit der Klasse gewertet. Dieser Argumentation kann entgegengehalten werden, dass ein Entwickler ggf. über die Freiheit verfügen möchte, die Instantiierung einer Klasse zu verhindern, selbst wenn sie vollständig implementiert ist. Zudem erleichtert ein verfügbares abstract-Schlüsselwort das einfache Erkennen abstrakter Klassen (durch Entwickler wie auch durch Werkzeuge).

Überraschenderweise ist es möglich, für eine pure virtual method eine Implementierung (sog. pure virtual implementation) in der abstrakten Klasse zu definieren. Mir ist bislang keine andere Sprache bekannt, die diesen Verstoß gegen das Prinzip der minimalen Verwunderung erlaubt.

Auch wenn Interfaces nicht explizit vorhanden sind, ist es freilich möglich, sie zu simulieren, indem alle Methoden einer Klasse als öffentliche pure virtual methods deklariert werden.

Objective-C (1983)

Objective-C kennt zwar das Schlüsselwort @interface, es wird jedoch lediglich zur Deklaration der Schnittstelle einer (konkreten) Klasse in einer Headerdatei eingesetzt, nicht zur Deklaration eines abstrakten Typs. Interfaces im Sinne abstrakter Typen sind in Objective-C ebenso wie abstrakte Klassen nicht verfügbar. Alle Klassen in Objective-C können instantiiert werden.

Object Pascal (1986)

Object Pascal kennt Interfaces und abstrakte Klassen. Die Deklarations-Syntax wird Pascal-üblich mittels des type-Schlüsselworts eingeleitet.
 
type
   MyInterface = interface(IInterface)
     // Methoden des Interfaces
   end;

Über das Schlüsselwort interface wird der Typ als Interface kenntlich gemacht. Zudem muss das Interface von einem Super-Interface ableiten, im obigen Beispiel das sprachweite Basisinterface IInterface. Eine Klasse, die dieses Interface implementieren soll, gibt dies wie folgt an:

type
  MyClass = class(Superklasse, MyInterface)

    // Details der Klasse
  end;


Die Angabe des Interfaces erfolgt, von einem Komma abgetrennt, hinter der Angabe der Superklasse. Es können beliebig viele Interfaces kommasepariert angegeben werden. Die Klasse wird hierdurch verpflichtet, alle Methoden der Interfaces zu implementieren. Abgesehen von der Syntax entspricht dies konzeptionell exakt der Verwendung von Interfaces in Java.

Abstrakte Klassen entstehen in Object Pascal dann, wenn mindestens eine Methode mittels der Schlüsselworte virtual; abstract; deklariert wird. Das sieht dann wie folgt aus:

type
  MyAbstractClass = class(Superklasse)

    public: 
      function myAbstractMethod: real; virtual; abstract;
  end;

Object Pascal folgt hier in zweierlei Hinsicht dem Verhalten von C++:
  • Methoden sind standardmäßig nicht überschreibbar, sondern müssen zu diesem Zweck explizit virtual deklariert werden (statically bound by default).
  • Klassen werden automatisch abstract, wenn eine abstrakte Methode deklariert ist. Das abstract-Schlüsselwort existiert nicht auf Klassen-Ebene. (In der 1993 vom Pascal Standards Committee veröffentlichten Object-Oriented Extension to Pascal wird auch das abstract-Schlüsselwort in den Sprachschatz aufgenommen. Die Deklaration erfolgt dann mittels MyAbstractClass = abstract class(Superklasse).
Eine weitere Eigenschaft verdient Erwähnung: Object Pascal verbietet Mehrfachvererbung in der Klassenhierarchie, erlaubt aber die Mehrfachableitung von Interfaces. Dieser Ansatz, die Probleme der Mehrfachvererbung zu vermeiden, ohne ganz auf sie zu verzichten, wurde später auch von Java und C# verfolgt. Details zur dieser Problematik werden unten im Abschnitt über Scala behandelt.

Eiffel (1986)

Eiffel kennt keine Interfaces, aber abstrakte Klassen, die dort deferred ( = aufgeschoben) heißen. Konkrete Klassen heißen in der Eiffel-Terminologie effective. Das Schlüsselwort sowohl für abstrakte Klassen als auch für abstrakte Methoden heißt dementsprechend deferred. Eine abstrakte Klasse mit abstrakter Methode sieht in Eiffel also wie folgt aus:

deferred class
    MYDEFERREDCLASS

feature
    myfeature: INTEGER

    deferred
    end
end


Ist mindestens eine Methode abstrakt, so muss auch die Klasse abstrakt sein. In früheren Eiffel-Versionen war es auch umgekehrt erforderlich, in Klassen, die abstrakt sind, mindestens eine abstrakte Methode zu deklarieren, doch diese Einschränkung wurde inzwischen aufgegeben. Eiffel ist dynamically bound by default, dementsprechend entfällt das virtual-Schlüsselwort.

Natürlich ist es in Eiffel wie auch in C++ möglich, Interfaces zu simulieren, indem in einer abstrakten Klasse ausschließlich abstrakte Methoden deklariert werden. Durch die Möglichkeit zur Formulierung von Vorbedingungen, Nachbedingungen und Invarianten (-> Design by Contract) gewinnt diese Form der Interface-Spezifikation gegenüber Interfaces als rein abstrakten Typen deutlich an Ausdruckskraft. 

In diesem Kontext ist zwar häufig von "Schnittstellen als Verträgen" die Rede, die gemeinten "Schnittstellen" sind aber nicht mit Interfaces im Sinne abstrakter Typen zu verwechseln. Design by Contract kann auch auf konkrete Klassen angewandt werden und ist somit als ein Konzept zu betrachten, das über die Möglichkeiten eines Typsystems deutlich hinausgeht.

Python (1991)

Python enthielt zunächst weder Interfaces noch abstrakte Klassen. Mit Python 2.6 wurden im Jahr 2008 allerdings Abstract Base Classes eingeführt. Eine ausführliche Begründung für diese Entscheidung liefert der Python Enhancement Proposal 3119. Da Abtract Base Classes streng genommen keine abstrakten Klassen sind (siehe unten), werde ich bei der folgenden Betrachtung, wie in der Python-Gemeinde üblich, jeweils nur von ABCs sprechen. Eine Klasse wird zu einer ABC, indem sie die Klasse abc.ABCMeta als Metaklasse deklariert:  

class MyAbstractClass(object): 
    __metaclass__ = abc.ABCMeta

Normalerweise ist type die Metaklasse einer deklarierten Python-Klasse. Klassen der Metaklasse type sind immer instantiierbar. Klassen der Metaklasse abc.ABCMeta waren in der ursprünglichen Konzeption nicht instantiierbar. Dieses Verhalten wurde in neueren Python-Versionen (vermutlich ab Python 3.x) allerdings wieder geändert. Mit Python 2.7 erhält man bei dem Versuch, eine Klasse wie die obere zu instantiieren den folgenden Laufzeitfehler:

TypeError: Can't instantiate abstract class MyClass with abstract methods mymethod

In aktuellen Python-Versionen (mein Versuch erfolgte mit Python 3.4.0) wird dies hingegen nicht mehr als Laufzeitfehler betrachtet. Sowohl die Instantiierung einer ABC als auch der Aufruf einer abstrakten Methode sind möglich und lassen das Programm klaglos seine Arbeit fortsetzen. Für diese Versionen ist es daher nicht mehr sinnvoll, von abstrakten Klassen zu sprechen.

Die Möglichkeit zur Deklaration von ABCs wird also nicht über ein Schlüsselwort, sondern über das Modul abc in Verbindung mit der Nutzung von Metaklassen in die Sprache eingeführt. Abstrakte Methoden werden wie folgt markiert: 

    @abc.abstractmethod 
    def mymethod(self):  
        ...

Eine ABC muss keine abstrakte Methode enthalten. Umgekehrt ist es jedoch kein Syntaxfehler, wenn eine Methode mit @abc.abstractmethod markiert wird, ohne dass die Klasse eine ABC ist.

Insgesamt dient die Einführung von abc.ABCMeta in Python primär der vereinfachten Prüfung der Schnittstellen-Eigenschaften eines Objekts. In PEP 3119 werden demnach auch die neuen Möglichkeiten zur Code-Inspection betont und nicht der Aspekt der unvollständigen Implementierung. ABCs dienen primär der Sicherstellung einer gemeinsamen Schnittstelle für eine gegebene Menge an Klassen. In dieser Hinsicht kann man es als konsequent ansehen, auf die Nicht-Instantiierbarkeit zu verzichten. Die Bezeichnung als abstrakte Klasse scheint mir unter diesen Umständen allerdings ein Verstoß gegen das Prinzip der minimalen Verwunderung zu sein.

Abschließend sollte noch erwähnt werden, dass das in Python realisierte Duck Typing keine neuen Aspekte in die hier angestellte Betrachtung abstrakter Typen einbringt. Duck Typing fügt den wesentlichen Eigenschaften abstrakter Typen (unvollständige Implementierung des abstrakten Typs, erforderliche Implementierung der abstrakten Methoden in Subklassen, Verhinderung der Instantiierung, Dependency Inversion) keine neuen Aspekte hinzu.

Java (1995)

Java kennt Interfaces und abstrakte Klassen. Eine Interface-Deklaration sieht wie folgt aus:

interface MyInterface extends Superinterface {
  void mymethod();
}

Die Methoden eines Interfaces sind alle implizit public und abstract. Da in Java alle Methoden (wenn sie nicht final oder private sind) virtuell und also überschreibbar (dynamically bound by default) sind, fehlt in Java das virtual-Schlüsselwort. Interfaces dürfen in Java Konstanten enthalten. Einer Klasse, die das Interface implementieren soll, wird dies mittels implements-Schlüsselwort mitgeteilt:

class MyClass extends Superclass implements MyInterface {
   void mymethod() {
   }
}

Für Klassen ebenso wie für Methoden stellt Java das Schlüsselwort abstract zur Verfügung. Eine Klasse kann abstract sein, ohne abstrakte Methoden zu enthalten.

Mit Java 8 wurden sogenannte Interface Default Methods eingeführt, die es ermöglichen, Methodenimplementierungen in Interfaces anzugeben. Die Definition einer Default-Methode sieht wie folgt aus:

default void myDefaultMethod() {
        ...
}

Durch Default-Methoden wird das Problem gemildert, dass die Einführung neuer Methoden in ein Interface zu Syntaxfehlern in allen Implementierungen führt, die nun gezwungen sind, die neuen Methoden zu implementieren. In der Vergangenheit erzwang dies oftmals ein Einfrieren einmal veröffentlichter Interfaces und führte z. T. zur Entstehung sogenannter I*2-Interfaces, die namentlich und inhaltlich identisch zu ihrem Orignal waren, bis auf das Suffix "2" und die hinzugefügten Methoden. Einer der ursprünglich diskutierten Namen für diesen neuen Methodentyp, Defender Method, macht diesen Sachverhalt deutlicher als der heute gewählte Name. Die Verwendung des default-Schlüsselworts ist übrigens obligatorisch, obwohl der Compiler Default-Methoden auch an ihrem Methodenkörper erkennen könnte. Hierdurch soll die leichtere Erkennbarkeit von Default-Methoden sichergestellt und die versehentliche Definition von Default-Methoden vermieden werden. Die Haupt-Motivation für die Einführung von Default-Methoden besteht also darin, die Evolution einmal veröffentlichter Interfaces offen zu gestalten. Es wird sich zeigen, ob sich auch andere sinnvolle Anwendungsmöglichkeiten entwickeln.

Das Diamond-Problem (siehe Abschnitt zu Scala unten), das sich aus der nun potentiell möglichen Mehrfachvererbung der Default-Methode ergibt, wird in Java übrigens so gelöst, dass eine Klasse, welche dieselbe Default-Methode aus zwei verschiedenen Interfaces erbt, gezwungen wird, eine eigene Implementierung der Methode zu definieren.

C# (2000)

C# lehnt sich hinsichtlich der Schlüsselwörter für abstrakte Typen eng an Java an. Wie dort steht für Klassen und Methoden abstract zur Verfügung. Abstrakte Klassen sind nicht verpflichtet, auch abstrakte Methoden zu enthalten. Enthält eine Klasse eine abstrakte Methode, so muss auch die Klasse explizit abstract deklariert werden. Mehrfachableitung ist nur für Interfaces möglich, die auch hier mittels interface deklariert werden. Im weiteren Kontext gibt es jedoch auch einige Unterschiede gegenüber Java:
  • Methoden sind in C# statically bound by default. Zwecks Überschreibung müssen sie daher normalerweise virtual deklariert werden. Für abstrakte Methoden ist dies jedoch nicht nötig - diese sind gewissermaßen "virtual by default". Tatsächlich können die Schlüsselworte abstract und virtual nicht gleichzeitig angewandt werden (anders als in Object Pascal, siehe oben).
  • Implementiert eine Klasse zwei Interfaces, welche dieselbe Methode deklarieren, so ist es im Gegensatz zu Java in C# möglich, zwei verschiedene Implementierungen für die beiden Interface-Methoden anzubieten. In der C#-Terminologie spricht man hier von "expliziter Interfaceimplementierung", engl. explicit interface implementation. C# löst dies, indem der Implementierung der Methode der Bezeichner des Interfaces vorangestellt wird (void MyInterface.mymethod() { ... }).  In Java kann nur eine gemeinsame Implementierung definiert werden. Durch explizite Interfaceimplementierung können zudem alle Klienten, welche die betreffende Methode verwenden wollen, gezwungen werden, die betreffenden Variablen mit dem Interface-Typ zu deklarieren. Eine per expliziter Interfaceimplementierung realisierte Methode ist nicht Teil des Klasseninterfaces der implementierenden Klasse.
  • Interfaces können in C# nicht nur von Klassen, sondern auch von Strukturen (Schlüsselwort struct) implementiert werden.
Es ist im Gegensatz zu C++ nicht möglich, innerhalb der abstrakten Klasse eine Implementierung für eine abstrakte Methode zu definieren.

Scala (2003)

Scala bietet abstrakte Klassen und Traits zur Deklaration abstrakter Typen an. Zur Kennzeichnung abstrakter Klassen steht das abstract-Schlüsselwort zur Verfügung. Abstrakte Klassen können nicht instantiiert werden. Die Deklaration einer abstrakten Methode erfordert auch die Deklaration einer abstrakten Klasse. Für Methoden kann das abstract-Schlüsselwort nicht genutzt werden. Während die Implementierung einer Methode in Scala normalerweise wie folgt aussieht

def mymethod(): Double = {
    return 2.0d 
}


wird für eine abstrakte Deklaration die "Zuweisung" eines Methodenkörpers einfach weggelassen:

def mymethod(): Double

Es wird nicht verlangt, in einer abstrakten Klasse mindestens eine abstrakte Methode zu deklarieren. Umgekehrt erfordert die Deklaration einer abstrakten Methode aber auch die Verwendung des abstract-Schlüsselworts für die Klasse. Methoden sind in Scala dynamically bound by default, so dass das virtual-Schlüsselwort entfällt. Allerdings muss auf der Seite der überschreibenden Methode die Überschreibung explizit mittels override def kenntlich gemacht werden, falls die überschriebene Methode nicht abstrakt ist.

In einschlägiger Scala-Literatur findet man häufig die Anmerkung, dass Scala Traits an Stelle von Interfaces anbietet oder dass sich Traits als "Interfaces mit konkreten Membern" vorstellen lassen. Dieser Gedankengang mutet allerdings etwas befremdlich an, wenn man Interfaces als rein abstrakte Typen versteht. Tatsächlich können Traits konkret und instantiierbar sein, wenn sie ausschließlich konkrete Methoden definieren. Der Vergleich bezieht sich somit eher auf die Möglichkeit zur Mehrfachvererbung, wie sie in Java und C# durch Interfaces eingeführt wurden, um die Konsequenzen des sogenannten Diamond-Problems zu vermeiden:



Angenommen die Klassen B und C überschreiben dieselbe Methode von A, dann ist unklar, welche dieser beiden Implementierungen D erben soll. In C++ führt dies zu sehr unschönen Eigenschaften. Java und C# haben sich elegant aus der Affäre gezogen. Scala führt mit Traits, die ursprünglich der Programmiersprache Self entstammen, die Möglichkeit zur Mehrfachvererbung wieder ein und vermeidet dabei das Diamond-Problem. Der Trick besteht dabei darin, dass die Deklarationsreihenfolge darüber entscheidet, welches Member von welchem Trait geerbt wird. Für unsere Diskussion sind allerdings nur die Interface-Eigenschaften von Traits relevant - also Traits ohne konkrete Member. Diese verhalten sich identisch zu Java-Interfaces und bieten auch wie diese die Möglichkeit zur Definition von Konstanten.

Zusammenfassung

Unser Streifzug durch die Nutzung abstrakter Typen hat uns an fast 50 Jahren Programmiersprachengeschichte vorübergeführt. Dabei zeigte sich, dass zahlreiche Realisierungsdetails abstrakter Typen in den verschiedenen Programmierspachen variieren:
  • Einige prominente objektorientierte Programmiersprachen bieten weder abstrakte Klassen noch Interfaces an. Dies gilt für Simula67, Smalltalk, Objective-C und Python. Python führte erst 2008 Abstract Base Classes ein, realisiert diese allerdings abgeschwächt über ein eigenständiges Modul und das Konzept der Metaklassen.
  • In einigen Programmiersprachen sind Methoden statically bound by default und erfordern es, abstrakte Methoden zusätzlich als virtual zu deklarieren. Hierzu gehören C++ und Object Pascal. In C# müssen konkrete Methoden zwecks Überschreibbarkeit virtual deklariert werden, während abstrakte Methoden implizit virtual sind.
  • C++ und Eiffel bieten Mehrfachvererbung auf Klassenebene an, stellen dafür aber keine Interfaces bereit. In Object Pascal, Java und C# steht Mehrfachvererbung nur für Interfaces zur Verfügung. Scala erlaubt die Mehrfachvererbung mittels Traits, vermeidet dabei aber das Diamond-Problem.
  • Bei Mehrfachvererbung mittels Interfaces ist es in einigen Sprachen (wie Java) nicht möglich, mehr als eine Implementierung anzugeben, wenn zwei Interfaces dieselbe Methode deklarieren. In anderen Sprachen (wie C#) ist dies hingegen durchaus realisierbar.
  • C++ und Object Pascal erfordern auf Grund des fehlenden abstract-Schlüsselworts für Klassen mindestens eine abstrakte Methode, um eine abstrakte Klasse zu deklarieren. Diese Notwendigkeit bestand früher auch in Eiffel, wurde inzwischen aber aufgegeben. In Python, Java, C# und Scala ist es möglich, abstrakte Klassen auch ohne abstrakte Methode zu deklarieren. Python erlaubt als einzige betrachtete Sprache die Markierung abstrakter Methoden auch in konkreten Klassen.
  • Außer C++ erlaubt keine andere Sprache pure virtual implementations, d. h. die Implementierung einer abstrakten Methode innerhalb der deklarierenden abstrakten Klasse.
  • Die Nutzung der zentralen Schlüsselworte im Zusammenhang mit abstrakten Klassen und Methoden sowie Interfaces variiert in Details. Die folgende Tabelle gibt abschließend eine Übersicht, wie in den verschiedenen Sprachen diese Deklarationen erfolgen.
  • In Java 8 wurde mit den sogenannter Interface Default Methods die Möglichkeit eingeführt, Methodenimplementierungen in Interfaces zu definieren.

Übersicht der Deklarationsweisen

Sprache Abstrakte Klassen Abstrakte Methoden Interfaces
Simula67 - - -
Smalltalk - - -
C++ Durch Vorhandensein abstrakter Methoden. Mittels virtual und „= 0“ an Stelle eines Methodenkörpers. -
Objective-C - - -
Object Pascal Durch Vorhandensein abstrakter Methoden. Mittels virtual und abstract. Mittels interface.
Eiffel Mittels deferred. Mittels deferred. -
Python Durch Angabe der Metaklasse abc.ABCMeta. Mittels Markierung durch @abc.abstractmethod. -
Java Mittels abstract. Mittels abstract. Mittels interface.
C# Mittels abstract. Mittels abstract. Mittels interface.
Scala Mittels abstract. Durch Weglassen des Methodenkörpers. Als Traits, die ausschließlich abstrakte Methoden enthalten, mittels trait.

Quellen

[Dahl1970] - Common Base Language, Ole-Johan Dahl, Bjørn Myrhaug (1970), http://www.fh-jena.de/~kleine/history/languages/Simula-CommonBaseLanguage.pdf

Keine Kommentare:

Kommentar veröffentlichen