"Binding" si riferisce all'atto di risolvere un nome di metodo in un pezzo di codice invocabile. Di solito, la chiamata alla funzione può essere risolta al momento della compilazione o al momento del collegamento. Un esempio di una lingua che utilizza il binding statico è 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. Ciò consente in anticipo alcune ottimizzazioni come l'inlining. I vantaggi più importanti sono:
- possiamo eseguire il controllo del tipo
- possiamo fare ottimizzazioni
D'altra parte, alcune lingue rimandano la risoluzione della funzione all'ultimo possibile momento. Un esempio è Python, in cui 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 rilegatura tardiva. Mentre effettua controlli di tipo rigorosi in modo irragionevole (il controllo dei tipi può essere eseguito solo in fase di runtime), è molto più flessibile e ci consente di esprimere concetti che non possono essere espressi entro i confini della tipizzazione statica o del binding anticipato. Ad esempio, possiamo aggiungere nuove funzioni in fase di runtime.
La distribuzione dei metodi come comunemente implementata nei linguaggi OOP "statici" è una via di mezzo tra questi due estremi: una classe dichiara in anticipo il tipo di tutte le operazioni supportate, quindi queste sono conosciute staticamente e possono essere digitate di conseguenza. Possiamo quindi creare una semplice tabella di ricerca (VTable) che punta all'attuazione effettiva. Ogni oggetto contiene un puntatore a un vtable. Il sistema di tipi garantisce che qualsiasi oggetto che otteniamo avrà un vtable adatto, ma non abbiamo idea in fase di compilazione quale sia il valore di questa tabella di ricerca. Pertanto, gli oggetti possono essere utilizzati per passare le funzioni in giro come dati (metà del motivo per cui OOP e programmazione delle funzioni sono equivalenti). Vtables può essere facilmente implementato in qualsiasi linguaggio che supporti i puntatori di 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 l'associazione anticipata e l'associazione tardiva. Considero il dispatch del metodo dinamico la proprietà di definizione centrale della programmazione OOP, con qualsiasi altra cosa (ad es. 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 un nuovo vtable e passare un oggetto con quel vtable a sayHelloToMeredith()
.
Sebbene si tratti di un attacco tardivo, non si tratta del "binding in ritardo estremo" preferito da Kay. Invece del modello concettuale "invio del metodo tramite i puntatori di funzione", utilizza "invio del metodo tramite il 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 ricevente può quindi provare a interpretare quel messaggio. Il sistema OOP più conosciuto è 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ò fornirmi la tua domanda.
Il potere del passaggio dei messaggi è che si adatta molto bene: nessun dato è condiviso (solo trasferito), tutto può accadere in modo asincrono e gli oggetti possono interpretare i messaggi come preferiscono. Questo rende un messaggio che passa il sistema OOP facilmente estensibile. Posso inviare messaggi che non tutti possono comprendere e recuperare il risultato atteso o un errore. L'oggetto non deve dichiarare in anticipo quali messaggi risponderà.
Ciò pone la responsabilità di mantenere la correttezza sul destinatario di un messaggio, un pensiero noto anche come incapsulamento. Per 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 es. se non ho i permessi. In OOP su scala più piccola, questo significa che non ho accesso in lettura / scrittura allo stato interno di un oggetto, ma devo passare attraverso metodi pubblici. Neanche un server HTTP deve servirmi un file. Potrebbe essere contenuto generato dinamicamente da un DB. In OOP reale, il meccanismo di risposta di un oggetto ai messaggi può essere rimosso senza che l'utente se ne accorga. Questo è più strong di "reflection", ma di solito un protocollo completo meta-oggetto. Il mio esempio C di cui sopra non può modificare il meccanismo di invio in fase di runtime.
La possibilità di modificare il meccanismo di invio implica l'associazione tardiva, poiché tutti i messaggi vengono instradati attraverso il codice definibile dall'utente. E questo è estremamente potente: dato un protocollo meta-oggetto, posso aggiungere funzionalità come classi, prototipi, ereditarietà, classi astratte, interfacce, tratti, eredità multipla, multi-dispatch, programmazione orientata all'aspetto, riflessione, invocazione di metodi remota, oggetti proxy ecc. in una lingua che non inizia con queste funzionalità. Questo potere di evoluzione è completamente assente dai linguaggi più statici come C #, Java o C ++.