Rilegatura ritardata orientata agli oggetti

9

Nella Definizione di Alan Kays di Object Oriented c'è questa definizione che parzialmente non capisco:

OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme LateBinding of all things.

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

    
posta Luca Zulian 06.11.2015 - 12:35
fonte

2 risposte

12

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

    
risposta data 06.11.2015 - 16:22
fonte
3

Il binding tardivo si riferisce al modo in cui gli oggetti comunicano tra loro. L'ideale che Alan sta cercando di ottenere è che gli oggetti siano il più liberamente accoppiati 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 le consente di crescere e cambiare organicamente.

Ad esempio, in C # potresti scrivere in un metodo per obj1 qualcosa come obj2.doSomething() . Puoi consultarlo come obj1 comunicando con obj2 . Affinché ciò avvenga in C #, obj1 deve conoscere un bel po 'di obj2 . Avrà bisogno di sapere la sua classe. Avrebbe controllato che la classe avesse un metodo chiamato doSomething e che ci fosse una versione di quel metodo che prende zero parametri.

Ora immagina un sistema in cui stai inviando un messaggio attraverso una rete o simili. potresti scrivere qualcosa come 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 riceverà la stringa "doSomething". Ma altrimenti sai ben poco.

Ora immagina che è così che gli oggetti comunicano. Conoscete un indirizzo e potete inviare messaggi arbitrari a quell'indirizzo con qualche tipo di funzione "postbox". In questo caso, obj1 non ha bisogno di sapere molto su obj2 , solo il suo indirizzo. Non ha nemmeno bisogno di sapere che comprende doSomething .

Questo è praticamente il punto cruciale dell'etichettatura tardiva. Ora, nelle lingue che lo usano, come Smalltalk e ObjectiveC, di solito c'è un po 'di zucchero sintattico per nascondere la funzione Postbox. Ma altrimenti l'idea è la stessa.

In C # è possibile replicarlo, in un certo senso, avendo una classe Runtime che accetta un riferimento e una stringa di oggetto e usa reflection per trovare il metodo e invocarlo (inizierà a complicarsi con argomenti e valori di ritorno ma sarebbe possibile anche se brutto).

Modifica: per dissipare un po 'di confusione in merito al significato del binding tardivo. In questa risposta mi riferisco al late binding poiché capisco che Alan Kay lo intendeva e lo ha implementato 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 in fase di compilazione.

    
risposta data 06.11.2015 - 15:11
fonte

Leggi altre domande sui tag