Dynamische Bindung

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 in A und einmal in B.
  • method1 wird dynamisch gebunden. Oder anders gesagt: B.method1 hat A.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 – hier A.
  • 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…

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.