E’ arrivato il momento di tradurre inheritance di Douglas Crockford. Si tratta del testo che più mi ha affascinato tra quelli che sono presenti sul suo sito. Mostra con semplicità le potenzialità del paradigma prototipale che sta alla base di Javascript. Se polimorfismo non fosse un termine troppo carico di significati in questo ambito lo userei.
Un appunto alla nota conclusiva aggiunta al pezzo da Crockford stesso, la trovate in fondo alla traduzione nel riquadro dal bordo nero. Sono daccordo solo parzialmente con l’autore quando scrive che i suoi tentativi di introdurre i pattern propri dei paradigmi dell’ereditarietà classica possono essere stati un errore. A parte la dimostrazione che Javascript può supportarli, non dobbiamo dimenticare che la stragrande maggioranza parte dei programmatori che si accingono ad utilizzare Javascript non lo acquisiscono come primo linguaggio e quindi hanno un’eredità di conoscenze che è una bella zavorra da portarsi dietro. Avere qualcosa di familiare a cui aggrapparsi mentre si svecchiano le proprie tecniche e si comincia a pensare in a Javascript way non può essere che un aiuto.
Ed ora non mi resta che augurarvi buona lettura
Ereditarietà Classica in Javascript
Titolo originale: Classical Inheritance in Javascript
Douglas Crockford (www.crockford.com)
JavaScript è un linguaggio orientato agli oggetti ma senza classi e, per questo motivo, supporta l’ereditarietà basata su prototipi al posto di quella tradizionale. Questo può lasciare perplessi i programmatori formati su linguaggi ad oggetti convenzionali come C++ e Java. Come vedremo, l’ereditarietà basata su prototipi ha una maggiore potenza espressiva di quella classica.
Ma per prima cosa chiediamoci, perché dovremmo preoccuparci dell’ereditarietà? Principalmente per due ragioni.
Java vs Javascript
Java | JavaScript |
---|---|
Tipi Forti | Tipi Deboli |
Statico | Dinamico |
Classico | Prototipale |
Classi | Funzioni |
Costruttori | Funzioni |
Metodi | Funzioni |
La prima è la convenienza dei tipi. Vogliamo che un linguaggio converta automaticamente i riferimenti di oggetti appartenenti a classi simili. Un sistema di tipi che richieda continuamente la conversione esplicita di riferimenti ad oggetti non assicura una buona sicurezza a livello dei tipi. Si tratta di una caratteristica di fondamentale importanza nei linguaggi fortemente tipizzati, ma che è irrilevante nei linguaggi debolmante tipizzati come Javascript, nei quali la conversione di tipi non è necessaria.
La seconda ragione è il riutilizzo. E’ comune avere molti oggetti che implementano tutti gli stessi metodi. Le classi permettono di creare tutti questi oggetti da un unico insieme di definizioni. E’ anche molto comune avere oggetti che sono simili a molti altri a meno dell’aggiunta o della modifica di un piccolo numero di metodi. L’ereditarietà classica è molto utile in queste circostanze ma quella prototipale lo è molto di più.
Per dimostrare queste mie affermazioni, introdurremo un pò di zucchero sintattico che ci permetterà di scrivere in uno stile simile ad un linguaggio convenzionale. Mostreremo poi degli utili pattern che non sono attuabili nei linguaggi classici. In ultimo spiegheremo com’è fatto lo zucchero.
Ereditarietà classica
Cominciamo creando una classe Parenizor
che ha i metodi set
e get
per il suo value
, ed un metodo toString
che racchiude il valore tra parentesi.
function Parenizor(value) {
this.setValue(value);
}
Parenizor.method('setValue', function (value) {
this.value = value; return this;
});
Parenizor.method('getValue', function () {
return this.value;
});
Parenizor.method('toString', function () {
return '(' + this.getValue() + ')';
});
La sintassi è un pò inusuale, ma è facile riconoscere il pattern classico su cui si basa. Il metodo method
prende in input il nome di un metodo e la funzione corrispondente, e li aggiunge alla classe come metodi pubblici.
In questo modo ci è possibile scrivere
myParenizor = new Parenizor(0);
myString = myParenizor.toString();
E, come ci si aspetta, mystring
sarà “(0)”.
Creiamo ora una nuova classe che erediti da Parenizor
, uguale alla sua genitrice ad eccezione del metodo toString
che produrrà “-0-” in caso il valore memorizzato nella classe sia vuoto o 0.
function ZParenizor(value) {
this.setValue(value);
}
ZParenizor.inherits(Parenizor);
ZParenizor.method('toString', function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
});
Il metodo inherits
è simile all’extends
di Java. Il metodo uber
corrisponde a super
in Java. Permette ad un metodo di invocare un metodo della classe progenitrice (I nomi sono stati cambiati per evitare restrizioni dovute a parole riservate).
Ora possiamo scrivere
myZParenizor = new ZParenizor(0);
myString = myZParenizor.toString();
E questa volta myString
varrà “-0-“.
Javascript non ha le classi ma possiamo programmarlo come se le avesse.
Ereditarietà multipla
Manipolando l’oggetto prototype
di una funzione, possiamo implementare l’ereditarietà multipla, potendo così costruire una classe composta dai metodi di diverse classi. L’ereditarietà multipla promiscua può essere difficile da implementare e può, potenzialmente, soffrire di collisioni tra i nomi dei metodi. Possiamo implementare l’ereditarietà multipla promiscua in Javascript, ma per questo esempio ne useremo una forma più disciplinata chiamata Swiss Inheritance
Supponiamo che ci sia una classe chiamata NumberValue
che ha un metodo setValue
che controlla che value
sia un numero appartenente ad un determinato intervallo, lanciando un’eccezione se necessario. Per il nostro ZParenizor
abbiamo bisogno di ereditare i metodi setValue
e setRange
da quella classe. Certamente non abbiamo bisogno del suo toString
. Per fare ciò potremmo scrivere
ZParenizor.swiss(NumberValue, 'setValue'. 'setRange');
Aggiungendo così alla classe solamente i metodi richiesti.
Ereditarietà parassita
Esiste un altro modo per scrivere ZParenizor
. Invece di ereditare da Parenizor
, scriviamo un costruttore che chiama il costruttore di Parenizor
, restituendo poi il risultato come se fosse proprio. Ed invece di aggiungere metodi pubblici, il costruttore aggiunge metodi privilegiati.
function ZParenizor2(value) {
var that = new Parenizor(value);
that.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-"
};
return that;
}
L’ereditarietà classica si basa sulla relazione è-un (is-a), mentre l’ereditarietà parassita si basa sulla relazione era-un-ora-è (was-a-but-now’s-a). Il costruttore ha un ruolo più ampio nella costruzione dell’oggetto. Notiamo anche che uber
e super
sono ancora disponibili ai metodi privilegiati.
Accrescimento di una classe
La dinamicità di Javascript ci permette di aggiungere o modificare i metodi di una classe esistente. Possiamo invocare il metodo method
in ogni momento e tutte le istanze, già esistenti o future, avranno il nuovo metodo. Possiamo letteralmente estendere una classe in qualunque momento. Chiamiamo questo processo Accrescimento di una classe Per evitare confusione con il costrutto extend
di Java, che ha un altro significato.
Accrescimento di un oggetto
In un linguaggio orientato agli oggetti statico, se abbiamo disogno di un oggetto leggermente diverso da un altro, dobbiamo definire una nuova classe. In Javascript, possiamo aggiungere metodi a singoli oggetti senza dover costruire classi aggiuntive. Questo ha un’enorme forza espressiva perché possiamo scrivere molte meno classi che possono essere molto più semplici. Gli oggetti Javascript sono come hashtable. Possiamo aggiungere nuovi valori in qualunque momento. Se il valore è una funzione, questo diventa un metodo.
Per questo motivo, nell’esempio che abbiamo appena fatto, non avevamo in realtà bisogno della nuova classe ZParenizor
. Avremmo potuto semplicemente modificare l’istanza appena creata
myParenizor = new Parenizor(0);
myParenizor.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
};
myString = myParenizor.toString();
Abbiamo aggiunto un metodo toString
all’istanza myParenizor
senza usare alcuna forma di ereditarietà. Possiamo far evolvere istanze singole di una classe proprio perché il linguaggio è senza classi.
Zucchero
Per far funzionare gli esempi qui sopra, Ho scritto quattro metodi che non sono altro che zucchero sintattico. Prima il metodo method
che aggiunge un metodo ad una classe.
Function.prototype.method = function (name, func) {
this.prototype[name] = func;
return this;
};
Questo codice aggiunge un metodo pubblico a Function.prototype
, il prototipo delle funzioni, così che tutte le funzioni lo acquisiscano per accrescimento. Prende un nome ed una funzione e li aggiunge al prototipo della funzione che lo invoca.
Il metodo restituisce this
. Quando scrivo un metodo che non ha bisogno di restituire un valore, solitamente faccio in modo che restituisca this
. Questo permette di usare uno stile di programmazione a cascata.
Subito dopo viene il metodo inherits
, che indica che una classe deriva da un’altra. Dovrebbe essere invocato dopo che entrambe le classi sono state definite ma prima che i metodi della classe che eredita vengano aggiunti.
Function.method('inherits', function (parent) {
var d = {}, p = (this.prototype = new parent());
this.method('uber', function uber(name) {
if (!(name in d)) {
d[name] = 0;
}
var f, r, t = d[name], v = parent.prototype;
if (t) {
while (t) {
v = v.constructor.prototype; t -= 1;
}
f = v[name];
} else {
f = p[name];
if (f == this[name]) {
f = v[name];
}
}
d[name] += 1;
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
d[name] -= 1;
return r;
});
return this;
});
Accresciamo nuovamente Function
. Creiamo una istanza della classe parent
e la usiamo come nuovo prototipo (prototype
). Correggiamo il campo constructor
, ed inoltre aggiugiamo il metodo uber
al prototipo.
Il metodo uber
cerca il metodo che gli viene specificato nel prototipo della classe. Questa è la funzione da invocare in caso di Ereditarietà parassita o accrescimento di un oggetto. Ma se stiamo utilizzando l’ereditarietà classica, dobbiamo trovare la funzione nel prototipo del parent
.
Il return
utilizza il metodo apply
per invocare la funzione, assegnando esplicitamente this
e passando un array di parametri. Otteniamo i parametri, se presenti, dall’array arguments
. Purtroppo, arguments
non è un vero array, e quindi bisogna usare nuovamente apply
per invocare il metodo slice
.
Per ultimo, in metodo swiss
.
Function.method('swiss', function (parent) {
for (var i = 1; i < arguments.length; i += 1) {
var name = arguments[i];
this.prototype[name] = parent.prototype[name];
}
return this;
});
Questo metodo scorre tutti i parametri che gli sono passati. Per ogni name
, copia un membro dal prototype
di parent
nel prototype
della nuova classe.
Conclusioni
Javascript può essere usato come un linguaggio classico, ma ha una forza espressiva che è quasi unica. Abbiamo discusso di ereditarietà classica, ereditarietà multipla (swiss), parassita ed accrescimento di classi ed oggetti. Questa pletora di pattern per il riutilizzo di codice vengono da un linguaggio che è considerato più piccolo e semplice di Java.
Gli oggetti classici sono rigidi. Il solo modo di aggiungere un nuovo membro a questo tipo di oggetti è creare una nuova classe. In Javascript gli oggetti sono malleabili. Per aggiungere un nuovo membro ad uno di questi ultimi basta un semplice assegnamento.
Visto che gli oggetti in Javascript sono così flessibili, è possibile pensare alle gerarchie di classi in maniera differente. Le gerarchie profonde non sono approriate al linguaggio. Quelle poco profonde sono efficienti ed espressive.
uber
. L’idea di super
è importante nell’ereditarietà classica, ma non sembra essere necessario nei paradigmi funzionale e protitipale. In questo momento vedo come un errore i miei primi tentativi di supportare il modello classico di ereditarietà in Javascript.
Ci sarebbe da migliorare l’italiano del testo, per lo più incomprensibile.
Dire così mi sembra un po’ generico. Cosa ti sembra incomprensibile? Che alternative proponi? Considera che la traduzione dovrebbe essere il più possibile aderente all’originale.
beh qualcosa da migliorare ci sarebbe… per esempio quando si parla di relazioni dopo is-a si cita la was-a-but-now’s-a, la traduzione è sbagliata (era-un-orE-è per errore di stampa probabilmente).
Sistemato il refuso, grazie per la segnalazione. C’è voluto un po’ perché worpress perde la formattazione degli spezzoni di codice dopo il salvataggio.
Oh, Se qualcuno fosse a conoscenza del modo di evitare di riformattare tutto ogni volta ….