Zu dem, was in der OOP oft als sehr kompliziert wahrgenommen wird, gehört wohl eindeutig die dynamische Bindung. Hier mal eine kurze Erläuterung dazu:
Statischer und dynamischer Typ
Um dynamische Bindung zu verstehen, ist es zuerst einmal notwendig, zwischen statischem und dynamischem Typ zu unterscheiden. Bei der Typisierung geht es immer um den Typ, der einer Variablen zugewiesen wird. Jede Variable hat einen Typ, der angibt, was die Variable beinhalten bzw. referenzieren kann.
Wird eine Variable vom Typ Foo
deklariert, kann sie alle Objekte vom Typ Foo
referenzieren. Dazu gehören aber auch alle Objekte von Subtypen. Ein Objekt eines Subtyps ist gleichzeitig auch immer Objekt aller Supertypen (Subtyppolymorphie). Beispiel:
1 2 3 4 5 6 7 | class Foo ... end class; class Bar extends Foo ... end class; |
Jedes Bar
ist ein Foo
. Interessant wird es jetzt, wenn eine Variable eine Referenz auf einen Subtyp enthält:
1 2 3 4 | myObject: Foo; // myObject kann Foo-Objekte aufnehmen myObject := new Foo.create(); // jetzt referenziert myObject ein Foo-Objekt ... myObject := new Bar.create(); // jetzt referenziert myObject ein Bar-Objekt |
In diesem Beispiel ist myObject
als Foo
deklariert, hat zur Laufzeit aber den Typ Bar
. Man nennt Foo
den statischen Typ von myObject
und Bar
den dynamischen Typ von myObject
. Wie fast immer bezieht sich „statisch“ auf Compiletime und „dynamisch“ auf Runtime. Wie oben ersichtlich kann sich der dynamische Typ einer Variablen zur Laufzeit ändern, der statische Typ bleibt immer gleich und ist zur Laufzeit i.d.R. noch nichtmal mehr bekannt.
Warum ist aber nun der statische Typ überhaupt nötig? Genau genommen ist er nicht nötig. Dynamisch typisierte Sprachen wie Ruby und PHP haben so etwas wie statische Typen auch nicht. Dort kann aber auch kein Compiler auf Typisierungsfehler prüfen. Die Prüfung geschrieht erst zur Laufzeit. So gesehen ist der statische Typ eine Art Heuristik um Typfehler frühzeitig zu erkennen. Ohne statischen Typ weiß der Compiler nicht, welche Methoden aufgerufen werden könnten und welche nicht. Den dynamischen Typ kann der Compiler noch nicht kennen, da dieser erst zur Laufzeit entschieden wird.
Dynamische Bindung
Wird nun auf myObject
eine Methode aufgerufen, gibt es prinzipiell zwei Möglichkeiten, welche Methode verwendet werden könnte. Die Methode aus Foo
oder eine gleichname Methode aus Bar
. Richtet sich der Methodenaufruf nach dem statischen Typ, spricht man von statischer Bindung, ansonsten von dynamischer Bindung.
In manchen Programmiersprachen kann man explizit angeben, welche Methoden statisch und welche dynamisch gebunden werden sollen. Das ist beispielsweise in Delphi, C# und C++ der Fall. Hier muss man Methoden mit dem Schlüsselwort virtual
deklarieren, um dynamische Bindung zu ermöglichen. Alle nicht-virtuellen Methoden werden demnach statisch gefunden. In andere Sprachen wie beispielsweise Java sind alle Methoden immer virtuell.
Beispiel:
1 2 3 4 5 6 7 8 9 | class Foo public virtual method doSomething(); ... end class; class Bar extends Foo override public method doSomething(); ... end class; |
myObject.doSomething();
ruft also Bar.doSomething
auf, da myObject
vom dynamischen Typ Bar
ist und Foo.doSomething
mit virtual
markiert ist. Bei der Subklasse Bar
ist dann noch das Schlüsselwort override
nötig. Die Schlüsselwörter sind dabei in Java nicht nötig, da in Java ja immer alle Methoden virtuell sind.
Was nicht dynamisch gebunden wird
Nicht alles wird dynamisch gebunden. Insbesondere werden i.d.R. statisch gebunden:
- Nicht-virtuelle Methoden (die will man ja statisch binden)
- Klassenmethoden (die in vielen Sprachen mit dem Schlüsselwort „static“ markiert sind (hier gibt es gar kein zugehöriges Objekt)
- Attribute (das wär zu Aufwändig zu realisieren; außerdem braucht man das eh kaum)
- private Methoden (die sind in Subklassen eh nicht sichtbar)
Die statische Bindung ist für den Compiler etwas einfacher zu realisieren und zudem marginal schneller. Das ist der Grund, warum man in manchen Sprachen wählen kann. Der Performanceunterschied ist aber so klein, dass er in Java wie gesagt ignoriert wird.
Virtual Method Table
Realisiert wird das Ganze über die so genannte Virtual Method Table (VMT). Jedes Objekt enthält eine Referenz auf eine Tabelle, in der alle virtuelle Methoden stehen. Wird nun eine virtuelle Methode aufgerufen, geschieht das nicht direkt. Stattdessen wird in der Tabelle nachgesehen und die dort verzeichnete Methode aufgerufen. Die VMT repräsentiert damit sozusagen den dynamischen Typ des Objekts.
„Überschreiben“
Diese technische Sichtweise ist auch verantwortlich für das Schlüsselwort override
. Wenn man einen Subtyp deklariert und eine Methode überschreibt, so wird zuerst die VMT des Supertyps kopiert und dann der Eintrag der überschriebenen Methode mit der Adresse der neuen Methoden eben überschrieben (So kann man sich das zumindest vorstellen). Bei dieser Bezeichnung ist also der Gedanke mehr ein technischer, programmiersprachlicher. Ich leite eine Subklasse ab, erbe alle Methoden und Attribute und kann einzelne Methoden überschreiben.
Passiert dieses Überschreiben nicht, beispielsweise indem man das override
nicht angibt, wird die Methode nicht überschrieben, sondern nur „verdeckt“. Auch das ist wieder ein programmiersprachlicher Ansatz. „Verdeckt“ heißt hier, ich kann in der Subklasse nicht mehr ohne Weiteres auf die Methode der Superklasse zugreifen. Die Methode der Superklasse ist aber nicht überschrieben, also „noch da“. Sie kann von der Superklasse aus immer noch aufgerufen werden; sie wurde nicht durch die neue Methode ersetzt.
Das „Überschreiben“ beschreibt also den programmiersprachlich-technischen Aspekt während „dynamische Bindung“ eher ein konzeptioneller Begriff ist.
Beispiel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | class A var myAttribute: Integer = 1; begin public virtual method method1(); begin printLn("A.method1: " + myAttribute); end method; public method method2(); begin printLn("A.method2: " + myAttribute); end method; public method method4(); begin method2(); end method; public method method5(); begin method1(); end method; ... end class; class B extends A var myAttribute: Integer = 2; begin override public method method1(); begin printLn("B.method1: " + myAttribute); end method; public method method2(); begin printLn("B.method2: " + myAttribute); end method; public method method3(); begin method2(); end method; ... end class; myObject: A; myObject := new B.create(); myObject.method1(); myObject.method2(); myObject.method3(); myObject.method4(); myObject.method5(); |
Die Ausgabe ist folgende:
1 2 3 4 5 | B.method1: 2 A.method2: 1 B.method2: 2 A.method2: 1 B.method1: 2 |
Der Grund dafür ist folgender:
- Attribute werden statisch gefunden.
myAttribute
gibt es also zwei Mal. Einmal inA
und einmal inB
. method1
wird dynamisch gebunden. Oder anders gesagt:B.method1
hatA.method1
überschrieben. Deshalb wird die neue Methode ausgeführt.method2
ist nicht virtuell und wird deshalb statisch gebunden. Der Aufruf bezieht sich auf den statischen Typ – hierA
.A.method2
ist verdeckt. Ein Aufruf in der Subklasse ruft die neue Version auf.method2
wird nicht überschrieben. Ein Aufruf in der Superklasse wird also weiterhin die alte Methode aufrufen.method1
wird überschrieben. Auch alter Code ruft die neue Methode auf.
Warum das Ganze?
Die ganze Geschichte mit dynamischer Bidnung scheint auf den ersten Blick unnötig kompliziert. Dennoch ist es ein zentraler Bestandteil der OOP. Erst damit lassen sich abstrakte Kopplungen u.ä. umsetzen. Ohne dynamische Bindung wäre die OOP ziemlich langweilig. Aber davon muss ich wann andermal erzählen…