«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 ++.