Quali sono i vantaggi di OOP basato su prototipo rispetto a OOP basato su classi?


47

Quando ho iniziato a programmare Javascript dopo aver affrontato principalmente OOP nel contesto di linguaggi basati su classi, sono rimasto confuso sul perché OOP basato su prototipi sarebbe mai stato preferito a OOP basato su classi.

  1. Quali sono i vantaggi strutturali dell'utilizzo di OOP basato su prototipo, se presenti? (ad esempio, ci aspettiamo che sia più veloce o meno dispendioso in alcune applicazioni?)
  2. Quali sono i vantaggi dal punto di vista di un programmatore? (ad es. È più facile codificare determinate applicazioni o estendere il codice di altre persone utilizzando la prototipazione?)

Si prega di non considerare questa domanda come una domanda su Javascript in particolare (che ha avuto molti difetti nel corso degli anni che non sono completamente correlati alla prototipazione). Invece, per favore guardalo nel contesto dei vantaggi teorici della prototipazione rispetto alle classi.

Grazie.


Risposte:


46

Ho avuto molta esperienza di entrambi gli approcci durante la scrittura di un gioco di ruolo in Java. Inizialmente ho scritto l'intero gioco usando OOP basato su classi, ma alla fine ho capito che questo era l'approccio sbagliato (stava diventando non mantenibile con l'espansione della gerarchia di classi). Ho quindi convertito l'intera base di codice in codice basato su prototipo. Il risultato è stato molto migliore e più facile da gestire.

Codice sorgente qui se sei interessato ( Tyrant - Java Roguelike )

Ecco i principali vantaggi:

  • È banale creare nuove "classi" : basta copiare il prototipo e cambiare un paio di proprietà e voilà ... nuova classe. Ho usato questo per definire un nuovo tipo di pozione, ad esempio in 3-6 righe di Java ciascuna. Molto meglio di un nuovo file di classe e un sacco di boilerplate!
  • È possibile costruire e mantenere un numero estremamente elevato di "classi" con un codice relativamente piccolo - Tyrant, ad esempio, aveva qualcosa come 3000 prototipi diversi con solo circa 42.000 righe di codice totale. È abbastanza sorprendente per Java!
  • L'ereditarietà multipla è semplice : basta copiare un sottoinsieme delle proprietà da un prototipo e incollarle sulle proprietà in un altro prototipo. In un gioco di ruolo, ad esempio, potresti volere che un "golem d'acciaio" abbia alcune delle proprietà di un "oggetto d'acciaio" e alcune delle proprietà di un "golem" e alcune delle proprietà di un "mostro non intelligente". Facile con i prototipi, prova a farlo con un'erarchia ereditaria ......
  • Puoi fare cose intelligenti con i modificatori di proprietà : inserendo la logica intelligente nel metodo generico "leggi proprietà", puoi implementare vari modificatori. Ad esempio, è stato facile definire un anello magico che ha aggiunto +2 di forza a chiunque lo indossasse. La logica per questo era nell'oggetto ring, non nel metodo "lettura forza", quindi hai evitato di dover fare molti test condizionali altrove nella tua base di codice (ad esempio "il personaggio che indossa un anello di forza aumenta?")
  • Le istanze possono diventare modelli per altre istanze , ad esempio se si desidera "clonare" un oggetto è facile, basta usare l'oggetto esistente come prototipo per il nuovo oggetto. Non è necessario scrivere molte logiche di clonazione complesse per classi diverse.
  • È abbastanza facile cambiare il comportamento in fase di esecuzione - cioè è possibile modificare una proprietà e "trasformare" un oggetto in modo praticamente arbitrario in fase di esecuzione. Permette fantastici effetti in-game, e se si accoppia questo con un "linguaggio di scripting", praticamente tutto è possibile in fase di esecuzione.
  • È più adatto a uno stile di programmazione "funzionale" : tendi a ritrovarti a scrivere molte funzioni che analizzano gli oggetti in modo appropriato, piuttosto che la logica incorporata nei metodi collegati a classi specifiche. Personalmente preferisco questo stile FP.

Ecco gli svantaggi principali:

  • Si perde l'assicurazione della tipizzazione statica , poiché si sta effettivamente creando un sistema di oggetti dinamico. Questo tende a significare che è necessario scrivere più test per assicurarsi che il comportamento sia corretto e che gli oggetti siano del "tipo" giusto
  • C'è un certo sovraccarico di prestazioni : poiché le letture delle proprietà degli oggetti sono generalmente costrette a passare attraverso una o più ricerche di mappe, si paga un leggero costo in termini di prestazioni. Nel mio caso non è stato un problema, ma potrebbe essere un problema in alcuni casi (ad esempio un FPS 3D con molti oggetti interrogati in ogni frame)
  • I refactoring non funzionano allo stesso modo : in un sistema basato su prototipi stai essenzialmente "costruendo" la tua eredità ereditaria con il codice. Gli IDE / strumenti di refactoring non possono davvero aiutarti poiché non riescono a migliorare il tuo approccio. Non ho mai trovato questo un problema, ma potrebbe sfuggire di mano se non stai attento. Probabilmente vuoi che i test controllino che la tua gerarchia ereditaria sia stata costruita correttamente!
  • È un po 'alieno : le persone abituate a uno stile OOP convenzionale possono facilmente confondersi. "Cosa vuoi dire che esiste una sola classe chiamata" Cosa "?!?" - "Come estendo questa classe finale di Cosa!?!" - "Stai violando i principi OOP !!!" - "È sbagliato avere tutte queste funzioni statiche che agiscono su qualsiasi tipo di oggetto!?!?"

Infine alcune note di implementazione:

  • Ho usato una HashMap Java per le proprietà e un puntatore "parent" per il prototipo. Funzionava bene ma presentava i seguenti aspetti negativi: a) le letture delle proprietà a volte dovevano risalire a una lunga catena genitore, danneggiando le prestazioni b) se si mutava un prototipo genitore, la modifica interesserebbe tutti i bambini che non avevano scavalcato la proprietà mutevole. Questo può causare bug sottili se non stai attento!
  • Se lo facessi di nuovo, utilizzerei una mappa persistente immutabile per le proprietà (un po 'come le mappe persistenti di Clojure ), o la mia implementazione della mappa hash persistente Java . Quindi otterresti il ​​vantaggio di copiare / modificare a buon mercato insieme a comportamenti immutabili e non dovrai collegare in modo permanente oggetti ai loro genitori.
  • Ci si può divertire se si incorporano funzioni / metodi nelle proprietà dell'oggetto. L'hacking che ho usato in Java per questo (sottotipi anonimi di una classe "Script") non era molto elegante - se lo facessi di nuovo probabilmente userei un linguaggio appropriato facilmente integrabile per gli script (Clojure o Groovy)


(+1) È una buona analisi. Inoltre, è più basato sul modello Java, ad esempio Delphi, C #, VB.Net ha proprietà esplicite.
umlcat,

3
@umlcat - Sento che il modello Java è praticamente lo stesso del modello Delphi / C # (a parte il simpatico zucchero sintattico per l'accesso alle proprietà) - devi ancora dichiarare staticamente le proprietà che desideri nella definizione della classe. Il punto di un modello prototipo è che questa definizione non è statica e non devi fare alcuna dichiarazione in anticipo ....
Mikera,

Questo ha perso un grande. È possibile modificare il prototipo, che altera efficacemente le proprietà in ogni singola istanza, anche dopo che sono state create senza toccare il costruttore del prototipo.
Erik Reppen,

Per quanto riguarda l'ereditarietà multipla che è più semplice, l'hai confrontata con Java che non supporta l'ereditarietà multipla, ma è più facile rispetto ai linguaggi che la supportano come C ++?
Piovezan,

2

Il vantaggio principale di OOP basato sui prototipi è che oggetti e "classi" possono essere estesi in fase di esecuzione.

In OOP basato sulla classe, ci sono molte buone caratteristiche, sfortunatamente, dipende dal linguaggio di programmazione.

Object Pascal (Delphi), VB.Net & C # ha un modo molto diretto di usare le proprietà (da non confondere con i campi) e i metodi di accesso alle proprietà, mentre Java e C ++, le proprietà sono accessibili tramite metodi. E PHP ha una miscela di entrambi, chiamati "metodi magici".

Esistono alcune classi di digitazione dinamica, sebbene i linguaggi OO della classe principale abbiano una digitazione statica. Penso che la tipizzazione statica con Class OO sia molto utile, perché consente una funzione chiamata Object Introspection che consente di creare quell'IDE e sviluppare pagine Web, visivamente e rapidamente.


0

Sono d'accordo con @umlcat. L'estensione della classe è un grande vantaggio. Ad esempio, supponiamo di voler aggiungere più funzionalità a una classe di stringhe per un lungo periodo di tempo. In C ++ lo faresti attraverso l'ereditarietà continua delle precedenti generazioni di classi di stringhe. Il problema con questo approccio è che ogni generazione diventa essenzialmente un tipo diverso, che può portare a una massiccia riscrittura delle basi di codice esistenti. Con l'ereditarietà prototipica devi semplicemente "associare" un nuovo metodo alla classe base originale ..., nessuna classe ereditata e relazioni ereditarie massicce ovunque. Mi piacerebbe vedere il C ++ con un meccanismo di estensione simile nel loro nuovo standard. Ma il loro comitato sembra essere gestito da persone che vogliono aggiungere funzionalità accattivanti e popolari.


1
Potresti voler leggere Monoliths "Unstrung" , std::stringha già troppi membri che dovrebbero essere algoritmi indipendenti o almeno non membri non amici. E comunque, è possibile aggiungere nuove funzioni membro senza modificare il layout in memoria se è possibile modificare la classe originale.
Deduplicatore,
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.