Objektorientierte Spätbindung

11

In der Alan Kays Definition von objektorientiert gibt es diese Definition, die ich teilweise nicht verstehe:

OOP bedeutet für mich nur Messaging, lokale Aufbewahrung und Schutz sowie das Verbergen von Staatsprozessen und extremes LateBinding aller Dinge.

Aber was bedeutet "LateBinding"? Wie kann ich dies auf eine Sprache wie C # anwenden? Und warum ist das so wichtig?

Luca Zulian
quelle
2
OOP in C # ist wahrscheinlich nicht die Art von OOP, die Alan Kay im Sinn hatte.
Doc Brown
Ich stimme Ihnen zu, absolut ... Beispiele sind in allen Sprachen willkommen
Luca Zulian

Antworten:

14

"Bindung" bezieht sich auf das Auflösen eines Methodennamens in einen aufrufbaren Code. Normalerweise kann der Funktionsaufruf zur Kompilierungszeit oder zur Verbindungszeit aufgelöst werden. Ein Beispiel für eine Sprache mit statischer Bindung ist C:

int foo(int x);

int main(int, char**) {
  printf("%d\n", foo(40));
  return 0;
}

int foo(int x) { return x + 2; }

Hier kann der Aufruf foo(40)vom Compiler aufgelöst werden. Dies ermöglicht frühzeitig bestimmte Optimierungen wie Inlining. Die wichtigsten Vorteile sind:

  • Wir können die Typprüfung durchführen
  • Wir können Optimierungen vornehmen

Andererseits verschieben einige Sprachen die Funktionsauflösung auf den letztmöglichen Moment. Ein Beispiel ist Python, wo wir Symbole im laufenden Betrieb neu definieren können:

def foo():
    """"call the bar() function. We have no idea what bar is."""
    return bar()

def bar():
    return 42

print(foo()) # bar() is 42, so this prints "42"

# use reflection to overwrite the "bar" variable
locals()["bar"] = lambda: "Hello World"

print(foo()) # bar() was redefined to "Hello World", so it prints that

bar = 42
print(foo()) # throws TypeError: 'int' object is not callable

Dies ist ein Beispiel für eine späte Bindung. Es macht zwar eine strenge Typprüfung unangemessen (die Typprüfung kann nur zur Laufzeit durchgeführt werden), ist jedoch weitaus flexibler und ermöglicht es uns, Konzepte auszudrücken, die nicht innerhalb der Grenzen der statischen Typisierung oder der frühen Bindung ausgedrückt werden können. Zum Beispiel können wir zur Laufzeit neue Funktionen hinzufügen.

Der in „statischen“ OOP-Sprachen üblicherweise implementierte Methodenversand liegt irgendwo zwischen diesen beiden Extremen: Eine Klasse deklariert den Typ aller unterstützten Operationen im Voraus, sodass diese statisch bekannt sind und typgeprüft werden können. Wir können dann eine einfache Nachschlagetabelle (VTable) erstellen, die auf die tatsächliche Implementierung verweist. Jedes Objekt enthält einen Zeiger auf eine vtable. Das Typsystem garantiert, dass jedes Objekt, das wir erhalten, eine geeignete vtable hat, aber wir haben zum Zeitpunkt der Kompilierung keine Ahnung, welchen Wert diese Nachschlagetabelle hat. Daher können Objekte verwendet werden, um Funktionen als Daten weiterzugeben (der halbe Grund, warum OOP und Funktionsprogrammierung gleichwertig sind). Vtables können problemlos in jeder Sprache implementiert werden, die Funktionszeiger unterstützt, z. B. C.

#define METHOD_CALL(object_ptr, name, ...) \
  (object_ptr)->vtable->name((object_ptr), __VA_ARGS__)

typedef struct {
    void (*sayHello)(const MyObject* this, const char* yourname);
} MyObject_VTable;

typedef struct {
    const MyObject_VTable* vtable;
    const char* name;
} MyObject;

static void MyObject_sayHello_normal(const MyObject* this, const char* yourname) {
  printf("Hello %s, I'm %s!\n", yourname, this->name);
}

static void MyObject_sayHello_alien(const MyObject* this, const char* yourname) {
  printf("Greetings, %s, we are the %s!\n", yourname, this->name);
}

static MyObject_VTable MyObject_VTable_normal = {
  .sayHello = MyObject_sayHello_normal,
};
static MyObject_VTable MyObject_VTable_alien = {
  .sayHello = MyObject_sayHello_alien,
};

static void sayHelloToMeredith(const MyObject* greeter) {
   // we have no idea what the VTable contents of my object are.
   // However, we do know it has a sayHello method.
   // This is dynamic dispatch right here!
   METHOD_CALL(greeter, sayHello, "Meredith");
}

int main() {
  // two objects with different vtables
  MyObject frank = { .vtable = &MyObject_VTable_normal, .name = "Frank" };
  MyObject zorg  = { .vtable = &MyObject_VTable_alien, .name = "Zorg" };

  sayHelloToMeredith(&frank); // prints "Hello Meredith, I'm Frank!"
  sayHelloToMeredith(&zorg); // prints "Greetings, Meredith, we are the Zorg!"
}

Diese Art der Methodensuche wird auch als "dynamischer Versand" bezeichnet und liegt irgendwo zwischen früher und später Bindung. Ich betrachte den dynamischen Methodenversand als die zentrale definierende Eigenschaft der OOP-Programmierung, wobei alles andere (z. B. Kapselung, Subtypisierung, ...) zweitrangig ist. Es ermöglicht uns, Polymorphismus in unseren Code einzuführen und einem Code sogar neues Verhalten hinzuzufügen, ohne ihn neu kompilieren zu müssen! Im C-Beispiel kann jeder eine neue vtable hinzufügen und ein Objekt mit dieser vtable an übergeben sayHelloToMeredith().

Während dies eine späte Bindung ist, ist dies nicht die von Kay favorisierte „extreme späte Bindung“. Anstelle des konzeptionellen Modells „Methodenversand über Funktionszeiger“ verwendet er „Methodenversand über Nachrichtenübermittlung“. Dies ist eine wichtige Unterscheidung, da das Weiterleiten von Nachrichten weitaus allgemeiner ist. In diesem Modell verfügt jedes Objekt über einen Posteingang, in den andere Objekte Nachrichten einfügen können. Das empfangende Objekt kann dann versuchen, diese Nachricht zu interpretieren. Das bekannteste OOP-System ist das WWW. Hier sind Nachrichten HTTP-Anforderungen und Server sind Objekte.

Zum Beispiel kann ich den programmers.stackexchange.se Server fragen GET /questions/301919/. Vergleichen Sie dies mit der Notation programmers.get("/questions/301919/"). Der Server kann diese Anfrage ablehnen oder mir einen Fehler zurücksenden oder mir Ihre Frage stellen.

Die Fähigkeit der Nachrichtenübergabe besteht darin, dass sie sehr gut skaliert werden kann: Es werden keine Daten gemeinsam genutzt (nur übertragen), alles kann asynchron erfolgen und Objekte können Nachrichten nach Belieben interpretieren. Dies macht ein OOP-System für die Nachrichtenübermittlung leicht erweiterbar. Ich kann Nachrichten senden, die möglicherweise nicht jeder versteht, und entweder mein erwartetes Ergebnis oder einen Fehler zurückerhalten. Das Objekt muss nicht im Voraus deklarieren, auf welche Nachrichten es antworten wird.

Dies überträgt die Verantwortung für die Aufrechterhaltung der Korrektheit auf den Empfänger einer Nachricht, ein Gedanke, der auch als Kapselung bezeichnet wird. Beispielsweise kann ich eine Datei nicht von einem HTTP-Server lesen, ohne sie über eine HTTP-Nachricht anzufordern. Dadurch kann der HTTP-Server meine Anfrage ablehnen, z. B. wenn mir Berechtigungen fehlen. In kleinerem OOP bedeutet dies, dass ich keinen Lese- / Schreibzugriff auf den internen Status eines Objekts habe, sondern öffentliche Methoden durchlaufen muss. Ein HTTP-Server muss mir auch keine Datei bereitstellen. Es könnte dynamisch generierter Inhalt aus einer Datenbank sein. In der realen OOP kann der Mechanismus, wie ein Objekt auf Nachrichten reagiert, ausgeschaltet werden, ohne dass ein Benutzer dies bemerkt. Dies ist stärker als "Reflexion", aber normalerweise ein vollständiges Metaobjektprotokoll. Mein C-Beispiel oben kann den Versandmechanismus zur Laufzeit nicht ändern.

Die Möglichkeit, den Versandmechanismus zu ändern, impliziert eine späte Bindung, da alle Nachrichten über benutzerdefinierten Code geleitet werden. Und das ist äußerst leistungsfähig: Mit einem Metaobjektprotokoll kann ich Funktionen wie Klassen, Prototypen, Vererbung, abstrakte Klassen, Schnittstellen, Merkmale, Mehrfachvererbung, Mehrfachversand, aspektorientierte Programmierung, Reflexion, Remote-Methodenaufruf, hinzufügen. Proxy-Objekte usw. in eine Sprache, die nicht mit diesen Funktionen beginnt. Diese Fähigkeit zur Weiterentwicklung fehlt in statischeren Sprachen wie C #, Java oder C ++ vollständig.

amon
quelle
4

Späte Bindung bezieht sich darauf, wie Objekte miteinander kommunizieren. Das Ideal, das Alan zu erreichen versucht, besteht darin, dass Objekte so locker wie möglich gekoppelt werden. Mit anderen Worten, dass ein Objekt das Minimum kennen muss, um mit einem anderen Objekt zu kommunizieren.

Warum? Denn das fördert die Fähigkeit, Teile des Systems unabhängig voneinander zu verändern, und ermöglicht es ihm, organisch zu wachsen und sich zu verändern.

In C # können Sie beispielsweise eine Methode für obj1etwas wie schreiben obj2.doSomething(). Sie können dies als obj1Kommunikation mit betrachten obj2. Damit dies in C # geschieht, obj1muss man einiges darüber wissen obj2. Es muss seine Klasse kennen. Es hätte überprüft, ob die Klasse eine aufgerufene Methode hat doSomethingund ob es eine Version dieser Methode gibt, die keine Parameter akzeptiert.

Stellen Sie sich nun ein System vor, auf dem Sie eine Nachricht über ein Netzwerk oder ähnliches senden. Sie könnten so etwas schreiben Runtime.sendMsg(ipAddress, "doSomething"). In diesem Fall müssen Sie nicht viel über die Maschine wissen, mit der Sie kommunizieren. Es kann vermutlich über IP kontaktiert werden und wird etwas tun, wenn es die Zeichenfolge "doSomething" empfängt. Aber sonst weißt du sehr wenig.

Stellen Sie sich nun vor, so kommunizieren Objekte. Sie kennen eine Adresse und können beliebige Nachrichten mit einer Art "Postfach" -Funktion an diese Adresse senden. In diesem Fall obj1muss nicht viel darüber wissen obj2, nur die Adresse. Es muss nicht einmal wissen, dass es versteht doSomething.

Das ist so ziemlich der Kern der späten Bindung. In Sprachen, die es verwenden, wie Smalltalk und ObjectiveC, gibt es normalerweise etwas syntaktischen Zucker, um die Postfachfunktion zu verbergen. Ansonsten ist die Idee dieselbe.

In C # können Sie es sozusagen replizieren, indem Sie eine RuntimeKlasse haben, die eine Objektreferenz und eine Zeichenfolge akzeptiert und mithilfe von Reflection die Methode findet und aufruft (es wird mit Argumenten und Rückgabewerten kompliziert, aber es wäre möglich hässlich).

Bearbeiten: um einige Verwirrung hinsichtlich der Bedeutung der späten Bindung zu beseitigen. In dieser Antwort beziehe ich mich auf die späte Bindung, da ich verstehe, dass Alan Kay sie gemeint und in Smalltalk implementiert hat. Es ist nicht die üblichere, modernere Verwendung des Begriffs, die sich allgemein auf den dynamischen Versand bezieht. Letzteres deckt die Verzögerung beim Auflösen der genauen Methode bis zur Laufzeit ab, erfordert jedoch zur Kompilierungszeit noch einige Typinformationen für den Empfänger.

Alex
quelle