Liaison tardive orientée objet

11

Dans la Définition d'Alan Kays d'Object Oriented, il y a cette définition que je ne comprends pas en partie:

Pour moi, la POO signifie uniquement la messagerie, la conservation et la protection locales et la dissimulation du processus d'état, et la liaison tardive extrême de toutes choses.

Mais que signifie "LateBinding"? Comment puis-je appliquer cela sur une langue comme C #? Et pourquoi est-ce si important?

Luca Zulian
la source
2
La POO en C # n'est probablement pas le genre de POO qu'Alan Kay avait en tête.
Doc Brown
Je suis d'accord avec vous, absolument ... les exemples sont les bienvenus dans toutes les langues
Luca Zulian

Réponses:

14

«Liaison» fait référence à l'acte de résoudre un nom de méthode en un morceau de code invocable. Habituellement, l'appel de fonction peut être résolu au moment de la compilation ou au moment de la liaison. Un exemple de langage utilisant une liaison statique est C:

int foo(int x);

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

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

Ici, l'appel foo(40)peut être résolu par le compilateur. Ce début permet certaines optimisations telles que l'inline. Les avantages les plus importants sont:

  • nous pouvons faire une vérification de type
  • nous pouvons faire des optimisations

En revanche, certains langages reportent la résolution des fonctions au dernier moment possible. Un exemple est Python, où nous pouvons redéfinir les symboles à la volée:

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

Ceci est un exemple de liaison tardive. Bien qu'il rend déraisonnable la vérification de type rigoureuse (la vérification de type ne peut être effectuée qu'au moment de l'exécution), il est beaucoup plus flexible et nous permet d'exprimer des concepts qui ne peuvent pas être exprimés dans les limites de la frappe statique ou de la liaison anticipée. Par exemple, nous pouvons ajouter de nouvelles fonctions lors de l'exécution.

La répartition des méthodes, telle qu'elle est généralement implémentée dans les langages POO «statiques», se situe quelque part entre ces deux extrêmes: Une classe déclare à l'avance le type de toutes les opérations prises en charge, elles sont donc statiquement connues et peuvent être vérifiées par type. Nous pouvons ensuite créer une table de recherche simple (VTable) qui pointe vers l'implémentation réelle. Chaque objet contient un pointeur vers une table virtuelle. Le système de types garantit que tout objet que nous aurons aura une table appropriée, mais nous n'avons aucune idée au moment de la compilation de la valeur de cette table de recherche. Par conséquent, les objets peuvent être utilisés pour transmettre des fonctions en tant que données (la moitié de la raison pour laquelle la POO et la programmation de fonctions sont équivalentes). Vtables peut être facilement implémenté dans n'importe quel langage prenant en charge les pointeurs de fonction, tels que 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!"
}

Ce type de recherche de méthode est également appelé «répartition dynamique», et quelque part entre la liaison anticipée et la liaison tardive. Je considère que la répartition dynamique des méthodes est la propriété centrale de définition de la programmation POO, avec quoi que ce soit d'autre (par exemple, encapsulation, sous-typage,…) comme secondaire. Il nous permet d'introduire du polymorphisme dans notre code, et même d'ajouter de nouveaux comportements à un morceau de code sans avoir à le recompiler! Dans l'exemple C, n'importe qui peut ajouter une nouvelle table virtuelle et passer un objet avec cette table virtuelle à sayHelloToMeredith().

Bien qu'il s'agisse d'une liaison tardive, ce n'est pas la «liaison extrêmement tardive» privilégiée par Kay. Au lieu du modèle conceptuel «envoi de méthode via des pointeurs de fonction», il utilise «envoi de méthode via passage de message». Il s'agit d'une distinction importante, car la transmission de messages est beaucoup plus générale. Dans ce modèle, chaque objet a une boîte de réception où d'autres objets peuvent placer des messages. L'objet récepteur peut alors essayer d'interpréter ce message. Le système OOP le plus connu est le WWW. Ici, les messages sont des requêtes HTTP et les serveurs sont des objets.

Par exemple, je peux demander au serveur programmers.stackexchange.se GET /questions/301919/. Comparez cela à la notation programmers.get("/questions/301919/"). Le serveur peut refuser cette demande ou me renvoyer une erreur, ou il peut me servir votre question.

La puissance du passage de message est qu'il évolue très bien: aucune donnée n'est partagée (seulement transférée), tout peut arriver de manière asynchrone et les objets peuvent interpréter les messages comme ils le souhaitent. Cela rend un message passant système OOP facilement extensible. Je peux envoyer des messages que tout le monde ne comprend pas et récupérer mon résultat attendu ou une erreur. L'objet n'a pas besoin de déclarer à l'avance à quels messages il répondra.

Cela met la responsabilité de maintenir l'exactitude sur le récepteur d'un message, une pensée également connue sous le nom d'encapsulation. Par exemple, je ne peux pas lire un fichier à partir d'un serveur HTTP sans le demander via un message HTTP. Cela permet au serveur HTTP de refuser ma demande, par exemple si je manque d'autorisations. Dans une POO à plus petite échelle, cela signifie que je n'ai pas accès en lecture-écriture à l'état interne d'un objet, mais que je dois passer par des méthodes publiques. Un serveur HTTP n'a pas non plus à me servir de fichier. Il peut s'agir de contenu généré dynamiquement à partir d'une base de données. Dans la vraie POO, le mécanisme de réponse d'un objet aux messages peut être désactivé, sans que l'utilisateur s'en aperçoive. C'est plus fort que la «réflexion», mais c'est généralement un protocole de méta-objet complet. Mon exemple C ci-dessus ne peut pas modifier le mécanisme de répartition lors de l'exécution.

La possibilité de modifier le mécanisme de répartition implique une liaison tardive, car tous les messages sont acheminés via un code définissable par l'utilisateur. Et cela est extrêmement puissant: étant donné un protocole de méta-objet, je peux ajouter des fonctionnalités telles que les classes, les prototypes, l'héritage, les classes abstraites, les interfaces, les traits, l'héritage multiple, la répartition multiple, la programmation orientée aspect, la réflexion, l'invocation de méthode à distance, objets proxy, etc. dans une langue qui ne démarre pas avec ces fonctionnalités. Ce pouvoir d'évoluer est complètement absent des langages plus statiques tels que C #, Java ou C ++.

amon
la source
4

La liaison tardive fait référence à la façon dont les objets communiquent entre eux. L'idéal qu'Alan essaie d'atteindre est que les objets soient couplés le plus librement possible. En d'autres termes, un objet doit connaître le minimum possible pour communiquer avec un autre objet.

Pourquoi? Parce que cela encourage la capacité de changer des parties du système indépendamment et lui permet de croître et de changer de façon organique.

Par exemple, en C #, vous pourriez écrire dans une méthode pour obj1quelque chose comme obj2.doSomething(). Vous pouvez considérer cela comme une obj1communication avec obj2. Pour que cela se produise en C #, il obj1faut en savoir un peu sur obj2. Il aura fallu connaître sa classe. Il aurait vérifié que la classe a une méthode appelée doSomethinget qu'il existe une version de cette méthode qui ne prend aucun paramètre.

Imaginez maintenant un système dans lequel vous envoyez un message sur un réseau ou similaire. vous pourriez écrire quelque chose comme Runtime.sendMsg(ipAddress, "doSomething"). Dans ce cas, vous n'avez pas besoin d'en savoir beaucoup sur la machine avec laquelle vous communiquez; il peut vraisemblablement être contacté via IP et fera quelque chose lorsqu'il recevra la chaîne "doSomething". Mais sinon, vous en savez très peu.

Imaginez maintenant que c'est ainsi que les objets communiquent. Vous connaissez une adresse et vous pouvez envoyer des messages arbitraires à cette adresse avec une sorte de fonction "boîte aux lettres". Dans ce cas, obj1n'a pas besoin d'en savoir beaucoup obj2, juste son adresse. Il n'a même pas besoin de savoir qu'il comprend doSomething.

C'est à peu près le nœud de la liaison tardive. Maintenant, dans les langages qui l'utilisent, tels que Smalltalk et ObjectiveC, il y a généralement un peu de sucre syntaxique pour masquer la fonction de boîte aux lettres. Mais sinon, l'idée est la même.

En C #, vous pouvez le répliquer, en quelque sorte, en ayant une Runtimeclasse qui accepte un objet ref et une chaîne et utilise la réflexion pour trouver la méthode et l'invoquer (cela va commencer à se compliquer avec des arguments et des valeurs de retour, mais ce serait possible si laid).

Edit: pour dissiper une certaine confusion quant à la signification de la liaison tardive. Dans cette réponse, je fais référence à une liaison tardive si je comprends bien ce qu'Alan Kay voulait dire et l'a implémentée dans Smalltalk. Ce n'est pas l'utilisation la plus courante et la plus moderne du terme qui fait généralement référence à la répartition dynamique. Ce dernier couvre le retard dans la résolution de la méthode exacte jusqu'à l'exécution, mais nécessite toujours certaines informations de type pour le récepteur au moment de la compilation.

Alex
la source