Rilegatura tardiva orientata agli oggetti


11

Nella definizione di orientamento agli oggetti di Alan Kays esiste questa definizione che in parte non capisco:

OOP per me significa solo messaggistica, conservazione locale, protezione e occultamento del processo statale ed estremo LateBinding di tutte le cose.

Ma cosa significa "LateBinding"? Come posso applicarlo su una lingua come C #? E perché è così importante?



2
OOP in C # probabilmente non è il tipo di OOP che Alan Kay aveva in mente.
Doc Brown,

Sono d'accordo con te, assolutamente ... gli esempi sono benvenuti in tutte le lingue
Luca Zulian,

Risposte:


14

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


4

L'associazione tardiva si riferisce al modo in cui gli oggetti comunicano tra loro. L'ideale che Alan sta cercando di raggiungere è che gli oggetti siano accoppiati il ​​più liberamente possibile. In altre parole, un oggetto deve conoscere il minimo possibile per comunicare con un altro oggetto.

Perché? Perché ciò incoraggia la capacità di cambiare parti del sistema in modo indipendente e gli consente di crescere e cambiare organicamente.

Ad esempio, in C # potresti scrivere un metodo per obj1qualcosa del genere obj2.doSomething(). Puoi considerarlo come obj1comunicare con obj2. Perché ciò avvenga in C #, è obj1necessario sapere un bel po ' obj2. Sarà necessario conoscere la sua classe. Avrebbe verificato che la classe abbia un metodo chiamato doSomethinge che esista una versione di quel metodo che accetta zero parametri.

Ora immagina un sistema in cui stai inviando un messaggio attraverso una rete o simile. potresti scrivere qualcosa del genere Runtime.sendMsg(ipAddress, "doSomething"). In questo caso non hai bisogno di sapere molto sulla macchina con cui stai comunicando; presumibilmente può essere contattato tramite IP e farà qualcosa quando riceve la stringa "doSomething". Ma per il resto sai molto poco.

Ora immagina che sia così che comunicano gli oggetti. Conosci un indirizzo e puoi inviare messaggi arbitrari a quell'indirizzo con una sorta di funzione "casella postale". In questo caso, obj1non c'è bisogno di sapere molto obj2, solo l'indirizzo. Non ha nemmeno bisogno di sapere che capisce doSomething.

Questo è praticamente il punto cruciale della rilegatura tardiva. Ora, nei linguaggi che lo usano, come Smalltalk e ObjectiveC, di solito c'è un po 'di zucchero sintattico per nascondere la funzione Postbox. Ma per il resto l'idea è la stessa.

In C # potresti replicarlo, in un certo senso, avendo una Runtimeclasse che accetta un oggetto ref e una stringa e usa la riflessione per trovare il metodo e invocarlo (comincerà a complicarsi con argomenti e restituire valori ma sarebbe possibile però brutto).

Modifica: per dissipare un po 'di confusione riguardo al significato di associazione tardiva. In questa risposta mi riferisco all'associazione tardiva per quanto ho capito che Alan Kay intendeva e implementava in Smalltalk. Non è l'uso più comune e moderno del termine che generalmente si riferisce alla spedizione dinamica. Quest'ultimo copre il ritardo nella risoluzione del metodo esatto fino al runtime ma richiede ancora alcune informazioni sul tipo per il ricevitore al momento della compilazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.