Experimentalia

Appunti raminghi

Ereditarietà classica in Javascript

with 6 comments

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.

Ad oggi ho programmato in Javascript per 8 anni, e non ho mai trovato un’occasione di usare la funzione 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.
About these ads

Written by Eineki

ottobre 13, 2009 at 3:41 am

Pubblicato su javascript, linguaggi, traduzioni

Tagged with ,

6 Risposte

Subscribe to comments with RSS.

  1. [...] C’è chi ribatte che Javascript non è un vero linguaggio ad oggetti perché non supporta l’ereditarietà. In realtà Javascript non si limita a supportare l’ereditarietà classica, ma permette anche altre forme … [...]

  2. [...] anni fa scrissi Ereditarietà classica in Javascript. Il testo mostrava che Javascript è un linguaggio ad oggetti senza classi, basato sui prototipi, e [...]

  3. Ci sarebbe da migliorare l’italiano del testo, per lo più incomprensibile.

    Ludovico

    dicembre 14, 2011 at 16:02 pm

  4. 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.

    Eineki

    dicembre 16, 2011 at 14:20 pm

  5. 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).

    john

    gennaio 25, 2012 at 11:07 am

  6. 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 ….

    Eineki

    gennaio 28, 2012 at 9:52 am


Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione / Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione / Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione / Modifica )

Google+ photo

Stai commentando usando il tuo account Google+. Chiudi sessione / Modifica )

Connessione a %s...

Iscriviti

Ricevi al tuo indirizzo email tutti i nuovi post del sito.

Unisciti agli altri 45 follower

%d blogger cliccano Mi Piace per questo: