Chiunque abbia sviluppato una piccola applicazione o una semplice interattività in JavaScript si sarà imbattuto almeno un volta in un metodo che prevedesse una funzione di callback. Strutture di questo tipo si incontrano molto spesso utilizzando librerie come jQuery:
$('a').click(function () {
//qui il codice della callback
});
Molti sviluppatori non trovano alcun problema nell'utilizzare questo pattern di programmazione nel loro codice e molto spesso la causa principale di tale comportamento è la poca importanza attribuita alla progettazione di applicazioni JavaScript e l'utilizzo del linguaggio come surrogato di Flash o panacea alle limitazioni del DOM.
In effetti, sebbene il codice in questione sia del tutto legittimo, esso introduce delle limitazioni architetturali che precludono la modularità e scalabilità dell'applicazione.
La più pesante conseguenza di tutto ciò è che spesso ci si trova a riscrivere intere porzioni di codice per assecondare un'unica modifica dell'applicazione.
Il problema delle funzioni anonime
Le limitazioni nell'uso di callback derivano principalmente dal fatto che troppo spesso vi si associano funzioni anonime, una caratteristica potente ma pericolosa del linguaggio JavaScript:
el.onclick = function () {
//questa è una funzione anonima
};
var miaFunzione = function () {
// anche questa è una funzione anonima
};
Queste funzioni non sono identificabili in nessun modo da altre parti dell'applicazione e quindi servono un unico scopo e molto spesso si accompagnano a codice duplicato:
el.onclick = function () {
var box = document.getElementById('box');
box.style.color = '#F00';
};
el2.onclick = function () {
// codice duplicato
var box = document.getElementById('box');
box.style.color = '#F00';
};
Un primo passo per ottimizzare il codice precedente è quello di instanziare una funzione assegnandole un nome:
function cambiaColore () {
var box = document.getElementById('box');
box.style.color = '#F00';
}
el.onclick = cambiaColore;
el2.onclick = cambiaColore;
Si capisce subito che non solo si diminuisce il peso del codice, ma si rende tutta l'applicazione più facilmente aggiornabile.
Un altro vantaggio non indifferente è che sarà più facile fare riferimento alla funzione di callback in fase di debug.
Il più vistoso effetto collaterale di questo approccio è che comporta la definizione di molti oggetti globali, con il rischio che altri script possano sovrascrivere le funzioni definite precedentemente.
Per ovviare a questo problema si può utilizzare una funzione self-executing attraverso la quale contenere le funzioni in un contesto (scope) privato:
(function () {
function cambiaColore () {
var box = document.getElementById('box');
box.style.color = '#F00';
}
function altraFunzione () {
//codice
}
el.onclick = cambiaColore;
el2.onclick = altraFunzione;
})();
oppure usare un oggetto come namespace generico:
var mieFunzioni = {
cambiaColore : function () {
var box = document.getElementById('box');
box.style.color = '#F00';
},
altraFunzione : function () {
//codice
}
};
el.onclick = mieFunzioni.cambiaColore;
el2.onclick = mieFunzioni.altraFunzione;
In quest'ultimo caso il vantaggio è che le funzioni definite come metodi dell'oggetto mieFunzioni
sono raggiungibili dal contesto globale e quindi riutilizzabili in altre parti dell'applicazione.
Programmazione ad eventi
Il secondo passo verso la scalabilità di un'applicazione è quello di astrarre le operazioni specifiche richieste per un'interazione dall'interazione stessa.
Ciò è possibile attraverso la progettazione dell'applicazione secondo la programmazione ad eventi, nella quale viene creato un programma centralizzato (dispatcher) al quale agganciare eventi personalizzati scatenati dall'interazione.
In questo modo una determinata funzione riguardante, ad esempio, l'apertura di una finestra di dialogo sarà lanciata all'evento apriDialogo
, piuttosto che da un click
o un submit
su uno specifico elemento dell'interfaccia.
Il vantaggio più evidente di questo approccio è che i cambiamenti dell'interfaccia non comportano un cambiamento nella logica dell'applicazione, inoltre permettono di agganciare una funzione a diversi tipi di eventi nel documento. Vediamo un esempio di implementazione in jQuery basato sul codice precedente:
var mieFunzioni = {
cambiaColore : function () {
$('#box').css('color', '#F00');
},
altraFunzione function () {
//codice
}
};
$(document).bind({
'cambiaColore' : mieFunzioni.cambiaColore,
'altroEvento' : mieFunzioni.altraFunzione
});
$('#mioEl').bind('click', function () {
$(document).trigger('cambiaColore');
});
$('#mioEl2').bind('click', function () {
$(document).trigger('altroEvento');
});
Nonostante questo script possa sembrare verboso, esso permette un livello di astrazione molto elevato:
- definisce un namespace
mieFunzioni
unico e globale per contenere il codice del programma; - aggancia ad un unico elemento (dispatcher) degli eventi personalizzati che non vanno ad interferire con gli eventi nativi del DOM;
- infine slega gli specifici eventi dei controlli dell'interfaccia (
onclick
,onsubmit
,onchange
) dall'evento che effettivamente lancia la funzione.
In questo caso l'uso di funzioni anonime per agganciare gli eventi personalizzati al click
non incide sull'applicazione, visto che la parte funzionale del codice è comunque richiamabile sia attraverso l'oggetto globale mieFunzioni
, sia attraverso il dispatcher $(document)
.
Vantaggi della programmazione ad eventi
I vantaggi di questo approccio sono molti. Anzitutto è possibile agganciare lo stesso evento ad altri elementi/eventi in maniera molto semplice:
$('#mioForm').bind('submit', function () {
$(document).trigger('cambiaColore');
});
Inoltre sarà molto più facile cancellare l'evento personalizzato senza doversi preoccupare dei controlli ai quali lo abbiamo associato:
$(document).unbind('cambiaColore');
//a questo punto cliccando su #mioEl o inviando #mioForm
//non succede più nulla
Applicazioni JavaScript modulari
Un altro vantaggio dell'uso di eventi personalizzati e della programmazione ad eventi è che ci permettono di progettare le applicazioni in maniera modulare.
Questo significa che potremmo pensare ad un'interfaccia come ad un insieme di piccole applicazioni indipendenti, legate fra loro per il tramite del dispatcher.
Per fare un esempio, immaginiamo di voler aggiungere un logger al documento in modo da registrare tutte le volte che viene cambiato il colore di #box
:
//<ul id="logger"></ul>
//creo il nuovo metodo
mieFunzioni.log = function () {
$('<li />')
.text('Il colore è stato cambiato!')
.appendTo('#logger');
};
//aggancio il metodo all'evento personalizzato
$(document).bind('cambiaColore', mieFunzioni.log);
Il risultato finale sarà che al click su #mioEl
o inviando #mioForm
verrà anche aggiunta una nuova riga al logger. I due moduli dell'applicazione sono indipendenti, in quanto nessuno dei due fa direttamente riferimento all'altro, tuttavia rispondono in maniera consistente nell'insieme dell'interfaccia.
Implementazioni nelle librerie
Finora abbiamo utilizzato jQuery per realizzare eventi personalizzati, tuttavia è possibile raggiungere gli stessi risultati con tutte le principali librerie JavaScript quali Mootools, Prototype e Dojo.
In particolare Dojo offre nativamente un'implementazione del dispatcher slegata dall'oggetto document
e gestibile attraverso i metodi .subscribe()
e .publish()
. Questo sistema, definito a Topics, è basato sulla creazione di canali attraverso i quali viene trasmesso l'evento e sui quali le funzioni collegate restano in ascolto:
//aggiungo una funzione in ascolto sul canale 'miocanale'
dojo.subscribe('miocanale', function (messaggio) {
alert('Nuovo messaggio: ' + messaggio);
});
//verrà mostrata una finestra di dialogo 'Nuovo messaggio: funziona!'
dojo.publish('miocanale', ['funziona!']);
Il vantaggio di questo sistema è di essere completamente slegato dal DOM e di non essere legato al suo contesto per quanto riguarda la proprietà this
, cosa che può creare dei problemi nelle altre implementazioni:
$(document).bind('mioEvento', function () {
//this è document
});
$('#mioForm').bind('submit', function () {
//this è #mioForm solo in questo contesto
//nel contesto di .trigger() diventa document
$(document).trigger('cambiaColore');
});
Conclusioni
Nonostante trovi spesso poca considerazione nell'ambiente degli sviluppatori web, JavaScript ha assunto negli ultimi anni un'importanza crescente, tale da veder aumentare le dimensioni e la complessità delle applicazioni basate su di esso.
Progettare e scrivere codice scalabile passando per l'uso di eventi personalizzati diventa perciò un requisito fondamentale per garantire affidabilità e longevità ai nostri progetti.
Di seguito alcuni interessanti link di approfondimento: