"Binding" si riferisce all'atto di risolvere un nome di metodo in un pezzo di codice invocabile. Di solito, la chiamata di funzione può essere risolta in fase di compilazione o in fase di collegamento. Un esempio di un linguaggio che utilizza l'associazione statica è C:
int foo(int x);
int main(int, char**) {
printf("%d\n", foo(40));
return 0;
}
int foo(int x) { return x + 2; }
Qui, la chiamata foo(40)
può essere risolta dal compilatore. Questo precoce consente alcune ottimizzazioni come inline. I vantaggi più importanti sono:
- possiamo fare il controllo del tipo
- possiamo fare ottimizzazioni
D'altra parte, alcune lingue rimandano la risoluzione della funzione all'ultimo momento possibile. Un esempio è Python, dove possiamo ridefinire i simboli al volo:
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
Questo è un esempio di associazione tardiva. Mentre rende irragionevolmente rigoroso il controllo del tipo (il controllo del tipo può essere eseguito solo in fase di esecuzione), è molto più flessibile e ci consente di esprimere concetti che non possono essere espressi entro i confini della tipizzazione statica o dell'associazione anticipata. Ad esempio, possiamo aggiungere nuove funzioni in fase di esecuzione.
L'invio di metodi come comunemente implementato nei linguaggi OOP "statici" si trova tra questi due estremi: una classe dichiara in anticipo il tipo di tutte le operazioni supportate, quindi queste sono staticamente note e possono essere selezionate per errore. Possiamo quindi creare una semplice tabella di ricerca (VTable) che punta all'implementazione effettiva. Ogni oggetto contiene un puntatore a una vtable. Il sistema di tipi garantisce che qualsiasi oggetto che otteniamo avrà una vtable adatta, ma al momento della compilazione non abbiamo idea di quale sia il valore di questa tabella di ricerca. Pertanto, gli oggetti possono essere utilizzati per trasferire funzioni come dati (metà del motivo per cui OOP e la programmazione delle funzioni sono equivalenti). Vtables può essere facilmente implementato in qualsiasi linguaggio che supporti i puntatori a funzioni, come 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!"
}
Questo tipo di ricerca del metodo è anche noto come "dispacciamento dinamico" e da qualche parte tra associazione anticipata e associazione ritardata. Considero l'invio di metodi dinamici come la proprietà di definizione centrale della programmazione OOP, con qualsiasi altra cosa (ad esempio incapsulamento, sottotipizzazione, ...) secondaria. Ci consente di introdurre il polimorfismo nel nostro codice e persino di aggiungere un nuovo comportamento a un pezzo di codice senza doverlo ricompilare! Nell'esempio C, chiunque può aggiungere una nuova vtable e passare un oggetto con quella vtable a sayHelloToMeredith()
.
Mentre questo è un legame tardivo, questo non è il "legame tardivo estremo" preferito da Kay. Invece del modello concettuale "invio di metodo tramite puntatori a funzione", usa "invio di metodo tramite passaggio di messaggi". Questa è una distinzione importante perché il passaggio dei messaggi è molto più generale. In questo modello, ogni oggetto ha una casella di posta in cui altri oggetti possono inserire messaggi. L'oggetto destinatario può quindi provare a interpretare quel messaggio. Il sistema OOP più noto è il WWW. Qui, i messaggi sono richieste HTTP e i server sono oggetti.
Ad esempio, posso chiedere al server programmers.stackexchange.se GET /questions/301919/
. Confronta questo con la notazione programmers.get("/questions/301919/")
. Il server può rifiutare questa richiesta o rispedirmi un errore oppure può rispondere alla tua domanda.
Il potere del passaggio dei messaggi è che si ridimensiona molto bene: nessun dato viene condiviso (solo trasferito), tutto può avvenire in modo asincrono e gli oggetti possono interpretare i messaggi come preferiscono. Ciò rende un messaggio che passa il sistema OOP facilmente estendibile. Posso inviare messaggi che non tutti possono capire e recuperare il risultato atteso o un errore. Non è necessario che l'oggetto dichiari in anticipo a quali messaggi risponderà.
Questo pone la responsabilità di mantenere la correttezza sul destinatario di un messaggio, un pensiero noto anche come incapsulamento. Ad esempio, non riesco a leggere un file da un server HTTP senza richiederlo tramite un messaggio HTTP. Ciò consente al server HTTP di rifiutare la mia richiesta, ad esempio se mi mancano le autorizzazioni. In OOP su scala ridotta, ciò significa che non ho accesso in lettura e scrittura allo stato interno di un oggetto, ma devo passare attraverso metodi pubblici. Neanche un server HTTP deve servirmi un file. Potrebbe essere generato dinamicamente contenuto da un DB. In OOP reale, il meccanismo di come un oggetto risponde ai messaggi può essere disattivato, senza che un utente se ne accorga. Questo è più forte della "riflessione", ma di solito è un protocollo meta-oggetto completo. Il mio esempio C sopra non può cambiare il meccanismo di invio in fase di esecuzione.
La possibilità di modificare il meccanismo di invio implica l'associazione tardiva, poiché tutti i messaggi vengono instradati attraverso un codice definibile dall'utente. E questo è estremamente potente: dato un protocollo meta oggetto, posso aggiungere funzionalità come classi, prototipi, ereditarietà, classi astratte, interfacce, tratti, ereditarietà multipla, invio multiplo, programmazione orientata all'aspetto, riflessione, invocazione di metodi remoti, oggetti proxy ecc. in una lingua che non inizia con queste funzionalità. Questo potere di evolversi è completamente assente da più linguaggi statici come C #, Java o C ++.