Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

JS.Class: portiamo in Javascript i vantaggi di Ruby

Analisi approfondita di un potente framework che consente di sfruttare pienamente le tecniche della programmazione ad oggetti
Analisi approfondita di un potente framework che consente di sfruttare pienamente le tecniche della programmazione ad oggetti
Link copiato negli appunti

Introduzione

Dopo aver analizzato negli articoli precedenti in maniera abbastanza dettagliata la tecnica della programmazione ad oggetti in Javascript è giunta l'ora di fare un passo avanti. Nonostante i piccoli accorgimenti evidenziati, l'utilizzo base del linguaggio di scripting presenta ancora qualche lacuna dal punto di vista della OOP e la rende ancora poco utilizzabile, soprattutto in ambito web.

In questo articolo analizzaremo in maniera abbastanza completa una delle migliori librerie presenti nel panorama JavaScript che facilitano l'utilizzo della programmazione ad oggetti tramite costrutti e funzioni ad hoc. Oltre a predisporre una base di lavoro funzionale il framework, che si chiama appunto JS.Class, espone alcune funzionalità in qualche modo "rubate" dal linguaggio del momento, Ruby, e le mette a disposizione di noi sviluppatori JavaScript. Basti pensare alla presenza di moduli e di mixin per rendersi conto di tutte le potenzialità della libreria. Inoltre sono presenti anche alcune classi di utility per implementare pattern di successo.

L'articolo è stato suddiviso in tre filoni principali. 

Nella prima sezione analizzeremo le funzionalità "core" del framework, ovvero quelle che permettono di realizzare una programmazione ad oggetti completa e stabile grazie alla presenza di ereditarietà, classi e Metodi statici, mixin, singleton e altre componenti.

La seconda parte sarà completamente dedicata ai moduli esposti da Js.Class che possono essere "mixati" alle nostre classi per aumentarne le funzionalità (per esempio implementare un sistema di comparazione tra due oggetti). Oltre ad analizzare i moduli pre-definiti, ne creeremo uno utilizzando gli strumenti presenti nella libreria.

L'ultima parte invece introdurrà alcuni pattern si sviluppo fondamentali della programmazione moderna e come è possibile implementarli grazie alle componenti sempre presenti in Js.Class.

Gli argomenti da affrontare sono molti e non sempre facili e banali. Per comprendere al meglio l'articolo suggerisco ai lettori di documentarsi un minimo su cosa intendiamo su programmazione ad oggetti e quali tecniche essa comporta.

Prima di iniziare credo che sia necessario in qualche modo citare l'autore del framework: James Coglan e ricordare a tutti che la libreria è stata rilasciata con una licenza MIT (per approfondire http://www.opensource.org/licenses/mit-license.php). La versione utilizzata per i nostri esperimenti è la 1.6.1. disponibile sul sito web ufficiale (http://jsclass.jcoglan.com).

Le basi di Js.Class

Il primo capitolo dell'articolo, come descritto in precedenza, si occuperà di analizzare i costrutti base esposti dalla libreria per realizzare una programmazione ad oggetti che sia consistente e funzionale, come se ci trovassimo in un linguaggio costruito apposta per essere OOP (cosa che come sappiamo JavaScript non è).

Creazione di classi e ereditarietà

La creazione di classi è possibile grazie all'oggetto Js.Class che riceve un numero di parametri opzionale ed è in grado sia di creare classi che di estenderne in maniera rapida:

var Person = Js.Class({
	initialize: function(name) {
		this.name = name;
	},
	drive: function(km) {
		alert("Start driving");
		for(var i = 1; i<=km; i++) alert("Km "+i);
		alert("Stop driving");
	},
	watchTv: function() {
		alert("I'm watching TV");
	}
});
var Man = Js.Class(Person, {
	watchTv: function() {
		alert("I'm watching a football match");
	}
});
var Woman = Js.Class(Person, {
	drive: function(km) {
		alert("Sorry but I can't drive");
	}
});

L'esempio proposto è abbastanza di facile comprensione: viene creata la classe Person che implementa, oltre al costruttore definito da initialize, il metodo drive() e il metodo watchTv(). Oltre a questa prima classe definiamo anche le classi Man e Woman che fanno l'override rispettivamente del metodo watchTv() e drive().

Oltre all'oggetto Js.Class, la libreria espone anche un particolare metodo chiamato callSuper() che permette, all'interno di un metodo, di chiamarne il padre in maniera automatica e senza dover ridefinire gli argomenti se non sono modificati dal metodo figlio.

var TaxyDriver = Js.Class(Man, {
	initialize: function(name, cpk) { //cost per km
		this.callSuper(name);
		this.cost = cost;
	}, 
	drive: function(km) {
		this.callSuper();
		alert("I earned " +  (km*this.cpk) + "euros");
	}
}

Il metodo callSuper() non è accessibile al di fuori della definizione dell'oggetto.

Le classi create tramite l'utilizzo dell'oggetto Js.Class presentano anche alcune funzionalità di introspezione, sempre utili in caso di applicazioni di una certa entità.

Il primo metodo appartenente a questa tipologia è isA() che è in grado di darci informazioni sulla tipologia di oggetto che stiamo trattando. Esso infatti torna un booleano che ci permette di identificare se un oggetto è o comunque estende una particolare classe:

var pippo = new Man("Pippo");
pippo.isA(Person); //true
pippo.isA(Man); //true
pippo.isA(Woman); //false

Ciascun oggetto inoltre presenta la proprietà klass che è un riferimento alla classe alla quale l'oggetto si riferisce mentre ogni classe presenta la proprietà superclass (che referenzia un eventuale classe padre) e il vettore subclasses (che referenzia eventuali classi figlio).

Man.klass //Man
Man.superclass; //Person
Person.subclasses; //[Man,Woman]

Metodi statici

Con metodi statici intendiamo dei particolari metodi che non vengono invocati su un'istanza di una particolare classe, ma sulla classe stessa. Solitamente ci si affida a questa tipologia di funzioni quando ci si riferisce a comportamenti che non riguardano una particolare istanza ma la classe generale, come per esempio la possibilità di recuperare una particolare istanza a partire dall'id o per funzioni di utilità generale.

Per implementare metodi statici utilizzando Js.Class basta utilizzare la proprietà extend all'interno della definizione della classe:

var ItalianCalendar = Js.Class({
	extends: {
		getDateFormat: function() {
			return "dd/mm/yyyy";
		}
	},
	[...]
});
var AmericanCalendar = Js.Class({
	extends: {
		getDateFormat: function() {
			return "mm/dd/yyyy";
		}
	},
	[...]
});
alert(ItalianCalendar.getDateFormat()); //stampo senza istanziare l'oggetto

All'interno dei metodi statici la variabile this fa riferimento alla classe generale e in caso di ereditarietà essi si comportano come fossero metodi normali: è possibile infatti overridarli e utilizzare il metodo dinamico callSuper().

Tutte le classi presentano inoltre un metodo extend() che permette di aggiungere membri statici alla classe anche successivamente rispetto alla sua definizione. Questo metodo è presente anche nelle varie istanze di oggetti creati grazie a Js.Class e permette di aggiungere funzioni solo ad un determinato oggetto. Facciamo un esempio:

var pippo = new Man("Pippo");
var topolino = new Man("Topolino");
Man.extend({
	explain: function() {
		alert("I'm the man class");
	}
});
pippo.extend({
	explain: function() {
		alert("I'm am Pippo");
	}
});
Man.explain(); //alert
pippo.explain(); //alert
topolino.explain(); //error

Mixin

La novità portata da Ruby sulla quale di più si è discusso è sicuramente la tecnica dei mixin. Tramite questo approccio è possibile creare delle classi particolari, chiamate Moduli, che implementano al loro interno delle funzionalità ma che non possono essere istanziate. L'unica possibilità di utilizzare queste funzionalità è proprio quella di "mixare" il modulo all'interno di una classe vera e propria. Grazie ai mixin è quindi possibile sopperire alla mancanza dell'ereditarietà multipla nei linguaggi moderni in quanto una classe può contemporaneamente estendere (e quindi ereditare le funzionalità) una classe e includere (quindi mixare le proprie funzionalità con quelle di un modulo) uno o più moduli.

I contesti dove l'utilizzo dei mixin diventa consigliato e ormai quasi obbligatorio sono quelli dove sono presenti diversi oggetti, tra di loro comunque non relazionati, ma che necessitano di avere alcune funzionalità in comune. Uno degli esempi più classici di mixin è il modulo Comparable. Grazie ad esso infatti è possibile includere in una classe funzionalità che permettono di effettuare confronti tra diverse istanze della classe senza in qualche modo modificare la classe stessa, che magari presenta già una propria ereditarietà.

Js.Class presenta nella sua architettura alcuni moduli base, sempre presi in prestito da Ruby. Nel secondo capitolo dell'articolo li approfondiremo maggiormente e proveremo a creare un nostro modulo.

Per includere un modulo in una particolare classe è possibile sia definire il mixin in fase di definizione, sia successivamente:

var Dog = Js.Create({
	include: Comparable,
	[...]
});
Dog.include(Comparable);

Tramite il secondo approccio è possibile inoltre passare un secondo argomento (boolean) per indicare se sovrascrivere o meno eventuali metodi con lo stesso nome già presenti nella classe.

Singleton

I singleton sono una particolare tipologia di oggetto che permette una sola istanziazione. Qualsiasi invocazione di un metodo infatti verrà eseguita sullo stesso oggetto in quanto non ci possono essere più copie dello stesso.

Questo pattern si adatta in quei contesti nei quali non è necessario istanziare diversi oggetti dalla stessa classe ma ne è sufficente uno solo. A differenza delle classi con metodi statici, tramite oggetti singleton è possibile sfruttare a pieno titolo l'ereditarietà.

Js.Class mette a disposizione un comodo costruttore per oggetti singleton:

var Application = Js.Singleton({
	openWindow: function(title) {
		[...]
	},
	openTab: function(tabIndex) {
		[...]
	}
});
Application.openWindow("Dettaglio");
Application.openTab("calendar");

Tutte le funzionalità esposte dalle classi Js.Class sono disponibili anche per le classi singleton (in particolare le proprietà klass, superclass e subclasses).

Interfacce

Quando si sviluppa un'applicazione di grandi dimensioni, spesso la comunicazione all'interno dei vari componenti del team di sviluppo è limitata. Utilizzando le interfacce è possibile in quale modo notificare agli altri componenti della squadra (sia presente che per una possibile manutenzione futura) che un determinato oggetto deve avere per poter appunto interfacciarsi con un determinato metodo.

Nei linguaggi propriamente object-oriented le interfacce rappresentano una componente core del programma e i controlli su di esse sono impliciti. In Javascript, non essendo un linguaggio completamente orientato agli oggetti, i controlli sulla consistenza di un particolare oggetto devono essere invocati "a mano".

Js.Class espone sia un costruttore particolare per la definizione di interfacce, sia un metodo statico per effettuare i controlli del caso.

Per definire un'interfaccia è necessario istanziare un oggetto Js.Interface definendo quali sono i metodi che l'oggetto che implementerà l'interfaccia dovrà per forza possedere:

var CarInterface = new Js.Interface([
	'repair', 'warmup', 'park'
]);

Invece per effettuare un controllo sulla presenza dei metodi sopra definiti:

function buyCar(car) {
	JS.Interface.ensure(car, CarInterface);
	[...]
}

In caso di mancanza dei metodi appartenenti all'interfaccia, il metodo ensure lancierà un'eccezione.

E' possibile effettaure un controllo multiplo su diverse interfacce semplicemente passando come parametri più riferimenti alle stesse.

Method binding

Una delle funzionalità più interessanti di Js.Class è il cosiddetto method binding, ovvero la possibilità di creare rapidamente riferimenti ad un metodo all'interno del proprio contesto di esecuzione. Ritornando al concetto degli scope, uno degli errori frequenti è quello di assegnare ad una callback di un evento il metodo di un oggetto JavaScript; metodo che però verrà invocato in uno scope differente rispetto a quello che il programmatore si aspetta.

Grazie al method binding, e in particolare al metodo method() è possibile superare questo ostacolo:

var Email = new Js.Class({
	initialize: function(from, to, text) {
		this.from = from;
		this.to = to;
		this.text = text;
	},
	read: function() {
		alert(this.from + "--->" + to + ": " + this.text);
	}
});
var myEmail = new Email("pippo@disney.com","topolino@disney.com","Ciao come stai??");
var button = document.createElement("button");
button.innerHTML = "(don't) Read Email";
button.onclick = myEmail.read;
var button2 = document.createElement("button");
button.innerHTML = "Read Email";
button.onclick = myEmail.method("read");

In questo caso la pressione del primo pulsante stamperà una stringa di questo tipo "undefined ---> undefined: undefined" perchè, come è stato piu volte ripetuto nei precedenti articoli, il metodo read non viene eseguito all'interno dell'oggetto myEmail, ma bensì dell'oggetto button. Utilizzando invece il metodo method() è possibile forzare lo scope del metodo.

Grazie al metodh binding è possibile assegnare metodi di classi particolari come callback di eventi gestiti dall'utente senza ricorrere a hack o a ulteriori librerie.

Il method binding è disponibile anche per metodi statici.

Hooks

Con il termine hooks (in italiano uncini) identifichiamo dei particolari metodi che vengono invocati automaticamente quando si verifica un particolare evento nella definizione di una classe. Js.Class presenta tre diverse tipologie di hooks. Tutti e tre i metodi vengono invocati automaticamente ricevendo come parametro la classe che ha scatenato l'evento.

Il primo è il metodo statico inherited() che viene invocato automaticamente quando la classe viene ereditata da qualche altra classe. 

Il secondo hook è il metodo included(). Questo metodo, implementabile per i moduli, viene invocato quando una classe include un modulo. 

Il terzo ed ultimo hook, extended(), è molto simile al precedente e viene invocato quando una particolare classe viene utilizzata per estenderne un'altra.

I contesti di utilizzo di questi particolari metodi sono abbastanza rari: spesso si utilizzano per funzioni di debug o di logging e per funzionalità di introspezione.

Mixin e Moduli

Js.Class oltre ad esporre funzionalità di basso livello per poter includere in una determinata classe uno o più moduli, presenta quattro moduli che possono essere facilmente integrati nelle nostre classi. Questi moduli sono una conversione di alcuni moduli presenti in Ruby, linguaggio al quale l'autore del framework si è ispirato.

Js.Comparable

Come descritto in precedenza, uno dei "classici" moduli presenti ormai in qualsiasi linguaggio che implementi il mixin è il modulo Js.Comparable. Grazie ad esso è possibile dotare, in maniera automatica, le nostre classi di un insieme di metodi di comparazione con altri oggetti facenti capo alla stessa classe.

Questo modulo si basa sulla definizione di un metodo chiamato compareTo(), che riceve come parametro un altro oggetto e che deve ritornare:

  • -1 se l'oggetto this è in qualche modo "inferiore" all'oggetto passato come parametro
  • 1 se l'oggetto this è in qualche modo "superiore" all'oggetto passato come parametro
  • 0 se i due oggetti sono "uguali"

Sono state utilizzate le virgolette proprio perchè gli oggetti sono sono propriamente comparabili tra di loro, ma lo diventano grazie all'inclusione del modulo Js.Comparable.

Per comprendere meglio l'argomento credo sia d'obbligo un esempio. Immaginiamo l'oggetto Fumetto che presenta come proprietà un numero di pagine e un boolean che rappresenta se è stato stampato a colori o in bianco e nero. Nella nostra pseudo-applicazione è presente la necessità di confrontare tra di loro dei fumetti per in qualche modo realizzare una classifica del valore di essi. Il valore è determinato appunto dal numero di pagine (più pagine ci sono, più acquista valore) e dal fatto che sia stampato a colori. Una possibile implementazione della classe potrebbe essere:

var Comic = Js.Create({
	initialize(pageNum, bn) {
		this.pageNum = pageNum;
		this.bn = bn;
	},
	compareTo(comic) { //scritto apposta in maniera prolissa per risultare leggibile
		if(this.pageNum > comic.pageNum) return 1;
		else if(this.pageNum < comic.pageNum) return -1;
		else {
			if(this.bn && !comic.bn) return 1;
			else if(comic.bn && !this.bn) return -1;
			return 0;
		}
	}
});

Una volta definito il metodo compareTo() è sufficente includere il modulo all'interno della nostra classe tramite il metodo include() (o includendo il modulo nella definizione della classe) per avere a disposizione, in maniera automatica, i seguenti metodi:

  • lt(o) ritorna true se this è minore dell'oggetto parametro
  • lte(o) ritorna true se thi è minore o uguale all'oggetto parametro
  • gt(o) ritorna true se this è maggiore dell'oggetto parametro
  • gte(o) ritorna true se this è maggiore o uguale all'oggetto parametro
  • eq(o) ritorna true se this è uguale all'oggetto parametro
  • between(o1, o2) ritorna true se l'oggetto è compreso tra i due oggetti parametri

Come è facilmente intuibile, l'utilizzo di un modulo facilita e velocizza notevolmente il lavoro.

Ecco un breve utilizzo del modulo:

var c1 = new Comic(100, true);
var c2 = new Comic(100, false);
var c3 = new Comic(150, true);
c1.gt(c2); //false
c3.lt(c1); //false
c2.between(c1, c3); //true

Un ulteriore vantaggio derivante dall'utilizzo del mixin è che non si obbliga il programmatore a dover ricorrere all'ereditarietà (che come sappiamo è limitante in quanto non multipla) per fattorizzare il codice ripetuto tra diverse classi.

Js.Enumerable

Il secondo modulo presente in Js.Class permette di definire una nostra classe come enumerabile, ovvero  come se fosse una collezione di entità. Prendendo sempre un esempio come soluzione per comprendere meglio l'utilizzo del modulo, possiamo riferirci ad una squadra di calcio che presenta al suo interno un vettore di giocatori. Includendo il modulo Js.Enumerable all'interno della classe è possibile gestire questo elenco di giocatori con una serie di metodi molto comodi.

Anche in questo caso per poter utilizzare il modulo è necessario implementare un metodo (forEach()) che poi verrà utilizzato internamente da Js.Enumerable. Il metodo riceve come parametro una funzione e uno scope e deve invocare la funzione per ciascun elemento della collezione, passando l'elemento corrente e l'indice come parametri:

var Team = Js.Class({
	initialize: function(players) {
		this.players = players;
	}, 
	forEach: function(func, scope) {
		for(var i = 0; i<this.players.length; i++) {
			func.call(scope, players[i], i);
		}
	},
	include: Js.Enumerable
}

Il modulo aggiungerà quindi alla nostra classe Team un insieme di metodi:

  • all(func, scope): ritorna true se l'invocazione della funzione (forzando eventualmente lo scope) ritorna true per ciascun elemento;
var milan = new Team("Maldini","Baresi","Tassotti","Panucci","Evani");
milan.all(function(el, i) {
	return el.length > 3;
}); //true
  • any(func, scope): ritorna true se l'invocazione della funzione (forzando eventualmente lo scope) ritorna true per almeno un elemento;
milan.any(function(el, i) {
	return el.length > 7;
}); //true
  • collect(func, scope): ritorna un array composto dal risultato dell'invocazione della funzione per ciascun elemento;
milan.collect(function(el, i) {
	return el.substring(0,1);
}); //["M","B","T","P","E"]

  • detect(func, scope): ritorna il primo elemento per il quale la funzione ritorna true;
milan.detect(function(el, i) {
	return el.indexOf("e") != -1;
}); //"Baresi"
  • entries(): ritorna un array contentente tutti gli elementi;
  • every(func, scope): alias per all(func, scope);
  • forEachCons(n, func, scope): chiama la funzione per ogni sottoinsieme composto di n elementi;
milan.forEachCons(3,function(list) {
	alert(list);
}); //["Maldini, "Baresi", "Tassotti"] + ["Baresi", "Tassotti", "Panucci"] + ["Tassotti", "Panucci", "Evani"]
  • forEachSlice(n, func, scope): divide gli elementi in tanti sotto-vettori di lunghezza n e chiama per ognuno di essi la funzione (se il numero di elementi rimanenti è inferiore a n, verranno comunque presi tutti);
milan.forEachSlice(2,function(list) {
	alert(list);
}); //["Maldini, "Baresi"] + ["Tassotti", "Panucci"] + ["Evani"]
  • filter(func, scope): ritorna un vettore composto dagli elementi che accettano la funzione di filtro;
milan.filter(function(el, i) {
	return el.length > 6;
}) //["Maldini","Tassotti","Panucci"]
  • find(func, scope): alias per detect(func, scope);
  • findAll(func, scope): alias per filter(func, scope);
  • inject(firstValue, func, scope): ritorna il risultato dell'invocazione della funzione per ciascun elemento (la funzione riceve oltre all'elemento corrente anche il risultato parziale)
milan.inject(5, function(value, el, i) {
	value += el.length;
}); //38 (dato dalla somma di 5+7+6+8+7+5)
  • map(func, scope): alias per collect(func, scope);
  • max(func, scope): ritorna l'elemento massimo della collezione (gli elementi devono includere Js.Comparable o essere comparabili tramite gli operatori standard di JavaScript). I parametri sono opzionali e identificano una funzione di ordinamento personalizzata;
  • member(el): ritorna true se il parametro è presente nella collezione;
milan.member("Ibrahimovic"); //false
  • min(func, scope): comportamento simile a max(func, scope) ma ribaltato;
  • partition(func, scope): ritorna due collezioni: la prima contenente gli elementi che ritornano true all'invocazione della funzione e la seconda gli altri;
milan.partition(function(el, i) {
	return el.length > 6;
}); //[ ["Maldini","Tassotti","Panucci"], ["Baresi","Evani"] ]
  • select(func, scope): alias per filter(func, scope);
  • reject(func, scope): comportamento simile a filter(func, scope) ma ribaltato: ritorna un vettore di elementi che non passano la funzione di filtro;
  • some(func, scope): alias per any(func, scope);
  • sort(func, scope): ritorna un vettore di elementi ordinato secondo la funzione passata o secondo i criteri standard di JavaScript (anche in questo caso gli elementi della lista devono includere Js.Comparable);
  • sortBy(func, scope): ritorna un vettore di elementi ordinati secondo l'indice ritornato dalla funzione;

milan.sortBy(function(el, i) {
	return el.length;
}); //["Evani","Baresi","Maldini","Panucci","Tassotti"]
  • toArray(): alias per entries();
  • zip(arrays, func, scope): metodo alquanto particolare: ritorna un insieme di vettori mischiando gli elementi con gli array passati come argomento (in caso di vettore di lunghezza inferiore, viene inserito null). Se viene definita una funzione, il metodo invece che tornare i vettori, invoca la funzione per ciascuno di essi;
milan.zip(["Materazzi","Carlos","Bergomi","Ferri","Matthaus"], ["Ferrara","Cabrini","Conte"]); 
/*[
["Maldini","Materazzi","Ferrara"],
["Baresi","Carlos","Cabrini"],
["Tassotti","Bergomi","Conte"],
["Panucci","Ferri",null],
["Evani","Matthaus",null]
] */

Js.Observable

JavaScript viene definito da molti come un linguaggio event-oriented, ovvero che si basa sulla presenza di eventi, ovvero comportamenti dell'utente che fanno scattare in automatico delle funzioni. Oltre agli eventi proposti dal linguaggio è possibile implementarne all'interno delle nostre classi per permettere di creare dei workflow complessi di azioni, tra diversi oggetti, ma che non vadano ad interferire sulla architettura dell'applicazione. Il modulo Js.Observable serve proprio a questo scopo; grazie ad esso è infatti possibile "notificare" un cambiamento avvenuto in un oggetto anche ad altri oggetti tramite funzioni di callback.

Il miglior modo per comprendere questo argomento è quello di un esempio. Immaginiamo due classi Team e Journalist, quando la prima realizza un gol vogliamo che il giornalista incaricato di seguirla venga notificato in maniera automatica del gol e possa cosi pubblicare la notizia in tempo reale sul sito del quotidiano presso il quale lavora. Iniziamo con la definizione delle classi:

var Team: Js.Class({
	include: Js.Observable,
	score: function(player) {
		this.golCount++;
		this.scorer.push(player);
		this.notifyObserver(this.name, player);
	},
	[...]
});
var Journalist: Js.Class({
	updateWebSite(team, player) {
		this.website.addArticle(player + " scored for " + team);
	}
});

Una volta definite le classi (da notare l'inclusione di Js.Observable alla classe che deve essere osservata e non all'osservatore) possiamo instanziare gli oggetti e assegnare il nostro giornalista come l'incaricato per la nostra squadra:

var j1 = new Journalist("Pippo");
milan.addObserver(j1.method('updateWebSite'));
milan.score("Inzaghi");

Con l'invocazione dell'ultima riga, non solo abbiamo modificato alcune proprietà dell'oggetto milan (golCount e scorer) ma abbiamo anche notificato il giornalista j1 (assegnato a seguire l'oggetto tramite addObserver()) del gol.

In questo modo è stato facile realizzare una sorta di flusso di informazioni condiviso tra due diversi oggetti senza però in qualche modo rimetterci in ordine e pulizia del codice. Il metodo addObserver() può essere invocato sia con un unico parametro (la funzione) sia con due aggiungendo anche lo scope. Queste due invocazioni si comportano allo stesso modo:

milan.addObserver(j1.method('updateWebSite'));
milan.addObserver(j1updateWebSite, j1);

Oltre al metodo addObserver() le classi che includono il modulo Js.Observable presentano anche il metodo removeObserver() e removeObservers() che rispettivamente eliminano l'osservatore passato come parametro o tutti gli osservatori assegnati in precedenza.

E' possibile invocare queste funzioni anche tramite i loro alias: subscribe() ed unsubscribe().

JS.Forwardable

Il quarto ed ultimo modulo inserito all'interno del framework presenta una particolarità rispetto ai precedenti: esso presenta un solo metodo statico e per questo motivo deve essere mixato con la classe custom tramite la direttiva extend e non come per i precedenti tramite include.Come abbiamo visto nel capito procedente infatti per definire membri statici di una classe abbiamo utilizzato extend.

JS.Forwardable si adatta alla perfezione per quegli oggetti che incapsulano al loro interno altri oggetti abbastanza complessi con propri metodi e proprietà. Esso infatti permette di creare una sorta di "ponte" tra l'oggetto principale e quello incapsulato, tramite dei metodi ad hoc creati appositamente per accedere all'oggetto interno direttamente dall'esterno. Guardiamo un esempio:

var Car = new JS.Class({
	initialize: function(brand, km) {
		this.brand = brand;
		this.km = km;
	},
	drive: function(km) {
	[...]
	},
	repair: function() {
	[...]
	}
});
var Man = new JS.Class({
	initialize: function(name, car) {
		this.name = name;
		this.car = car;
	},
	jump: function() {
	[...]
	}
});
var m = new Man("Marco", new Car("clio", 1000));

In questo contesto, per invocare uno dei metodi della classe Car è necessario utilizzare la nozione:

m.car.repair();

Questo approccio spesso può sembrare comodo, ma spesso è fonte di errori relativi alla visibilità dei vari membri di una classe. Grazie a JS.Forwardable è possibile invece creare dei metodi ad hoc che invocano il metodo corretto dell'oggetto incapsulato. Se volessimo esporre i metodi drive() e repair() basterebbe:

Car.extend(JS.Forwardable);
Car.defineDelegators('car', 'drive', 'repair');

Dopo aver incluso (in questo caso con extend) il modulo, abbiamo invocato il metodo statico defineDelegators che permette di aggiungere un insieme di metodi alla classe che wrappano i metodi della proprietà passata come primo parametro alla funzione. Grazie al metodo statico ora è possibile invocare i metodi drive() e repair() anche sugli oggetti di tipo Man.

Creiamo il nostro modulo: Debuggable

Dopo aver analizzato nello specifico i quattro moduli inseriti di default nel framework è giunto il momento di provare a realizzarne uno da zero utilizzando i componenti offerti da JS.Class. 

Il modulo che andremo a realizzare si chiama Debuggable e permette di includere all'interno delle nostre classi tre metodi che permettono di loggare in base a tre determinati livelli di verbosità. I livelli che ho predisposto per questo esempio sono i classici info, warn ed error. Il messaggio di logging verrà mostrato all'interno di un logWriter che non è nient'altro che un particolare elemento HTML definito attraverso un metodo. Sarà possibile inoltre definire un livello di logging desiderato per filtrare automaticamente eventuali messaggi non più desiderati.

Per poter essere integrato in una nostra classe, essa dovrà presentare il metodo getLogWriter() che deve ritornare appunto un elemento HTML presente nella pagina che verrà utilizzato da Debuggable per stampare i messaggi di log. Ogni istanza di questa classe dovrà inoltre presentare la proprietà logLevel che rappresenterà appunto la verbosità richiesta secondo questo semplice schema:

  • logLevel = 3: info, warn, error
logLevel = 2: warn, error
logLevel = 1: error
logLevel = 0: nessun messaggio loggato

Affrontiamo lo sviluppo di questo piccolo modulo con un approccio top-down, partendo quindi dal suo utilizzo per poi spingerci fino allo sviluppo a più basso livello. Una possibile classe che include Debuggable potrebbe essere questa:

BankAccount = new JS.Class({ 
	include: Debuggable, 
	initialize: function(balance) { 
		this.balance = balance; 
	}, 
	getLogWriter: function() { 
		return document.getElementById("logger"); 
	}, 
	withdraw: function(money) { 
		if(isNaN(money)) { 
			this.logError("Money parameter must be a number"); 
			return; 
		} 
		this.balance -= money; 
		var mess = "Withdraw done with success. Your balance is now " + this.balance; 
		if(this.balance > 0) { 
			this.logInfo(mess); 
		} else { 
			this.logWarn(mess); 
		} 
	} 
});

La classe BankAccount presenta infatti la dichiarazione di inclusione e il metodo getLogWriter(). Il metodo principale è withdraw che permette di effettuare un prelievo che andrà ad influenzare la proprietà balance. In caso di parametro non numerico la classe loggerà un errore, in caso di conto corrente in rosso si verificherà un log di warning, altrimenti un semplice log informativo.

L'utilizzo di questa classe in una pagina HTML è abbastanza semplice e non credo necessiti ulteriori specifiche:

<script type="text/javascript"> 
var myAccount = new BankAccount(100); 
myAccount.logLevel = 3; 
</script> 
<input type="text" name="withdraw"/> 
<button onclick="myAccount.method('withdraw')(document.getElementsByTagName('input')[0].value)">Withdraw</button> 
<div id="logger" style="border: 1px solid blue; ;overflow:auto"></div> 

Le uniche cose da sottolineare sono l'impostazione del logLevel pari a 3 e l'utilizzo del method binding all'interno dell'evento onclick. 

Passiamo ora all'implementazione del modulo Debuggable:

Debuggable = new JS.Module({ 
	logInfo: function(mess) { 
		this.log(mess, 3); 
	}, 
	logWarn: function(mess) { 
		this.log(mess, 2); 
	}, 
	logError: function(mess) { 
		this.log(mess, 1); 
	}, 
	log: function(mess, level) { 
		if(this.logLevel >= level) { 
			var logColor; 
			switch(level) {				 
				case 1: logColor = "red"; break; 
				case 2: logColor = "blue"; break; 
				case 3: default: logColor = "black"; 
			} 
			this.getLogWriter().innerHTML += "<br/><span style='color:"+logColor+"'>"+mess+"</span>"; 
		} 
	} 
});

Anche in questo caso non credo ci sia bisogno di particolari commenti sullo script in quanto abbastanza semplice. Da notare solamente lo switch che in base al livello di logging utilizza un colore diverso per dare risalto ai log più "gravi".

Per provare il modulo appena descritto ecco i sorgenti da scaricare.

Pattern di sviluppo

Il framework oltre a permettere di godere di tutti i vantaggi della programmazione ad oggetti anche all'interno del nostro amato JavaScript, espone alcuni particolari oggetti, realizzati sempre in ottica OOP grazie al framework stesso, che garantiscono e facilitano l'implementazione di alcuni diffusi pattern di sviluppo.

Questi pattern verranno analizzati solo in via generale, soprattutto per completezza riguardo a questo corposo framework. Per eventuali approfondimenti non posso che rimandare alla ottima documentazione (in inglese) presente sul sito ufficiale del progetto JS.Class.

JS.Command

Pattern che permette di definire degli oggetti che rappresentano le azioni scatenabili all'interno della nostra applicazione. Grazie ad essi è infatti possibile definire un dato comportamento ed associarlo a più eventi in maniera automatica in puro stile don't repeat yourself. Grazie all'oggetto JS.Command è inoltre possibile definire funzioni di undo per ripristinare lo stato dell'applicazione come se l'azione non fosse mai stata eseguita.

JS.Decorator

Pattern che permette di creare classi che "decorano" altre classi, aggiungendo funzionalità ed evitando di duplicare parti di codice. I decorator sono una valida alternativa quando non è possibile utilizzare l'ereditarietà.

JS.LinkedList

 Più che un pattern, le linked list sono una particolare struttura dati che permettono di rappresentare sequenze di oggetti. Possono essere paragonate a dei vettori solo che al posto dell'indice numerico, presentano riferimenti diretti agli oggetti successivi nella sequenza. JS.Class presenta sia un'interfaccia (appunto LinkedList) che una particolare implementazione (JS.LinkedList.Doubly.Circular).

JS.MethodChain

Pattern che permette di creare delle catene di metodi invocabili in maniera atomica come delle vere e proprie transizioni.

JS.Proxy

Un proxy è un particolare oggetto che permette di accedere ad un altro oggetto tramite un'interfaccia comune senza però modificare l'oggetto originale. Vengono spesso utilizzati per limitare gli accessi ai membri di un oggetto o per rappresentare in locale oggetti che possono risiedere in remoto. JS.Class presenta l'oggetto JS.Proxy.Virtual per creare proxy a partire da oggetti già istanziati.

JS.State

Pattern che permette di creare oggetti che presentano dei comportamenti differenti in base al loro stato attuale. Questo è un pattern abbastanza complesso e spesso fuorviante.

Conclusioni

Questo lungo articolo ci ha permesso di analizzare abbastanza nel dettaglio una delle librerie più complete e allo stesso tempo meno invasive presenti sul panorama JavaScript. Grazie ad essa è possibile beneficiare di tutti i vantaggi in termini di manutenibilità e portabilità offerti da una tecnica ormai consolidata e diffusissima come quella della programmazione orientata agli oggetti.

Spesso è possibile infatti incappare in librerie JavaScript complete ma che allo stesso tempo limitano l'utilizzo e la creatività da parte del programmatore che si trova molto spesso legato e molte volte deve ricorrere a soluzioni di comodo per affrontare un problema che magari non è stato preso in considerazione dagli autori del framework.

Spero che l'articolo, nonostante la sua lunghezza e complessità, non sia risultato noioso ma che abbia dato una spinta in senso "oggettistico" anche ai programmatori JavaScript meno amanti di questa tecnica di programmazione.

JS.Class 2.0: appendice

Alcuni giorni prima dell'uscita dell'articolo riguardante la versione 1.6 del framework JS.Class, l'autore ha rilasciato, sempre tramite licenza MIT, la versione 2.0 della libreria. Questa major release presenta alcuni aspetti importanti che necessitano di un approfondimento.

Il codice della versione 2.0 non è completamente retro-compatibile con la versione 1.6, però l'autore ha elencato i pochi aspetti che sono cambiati in modo da aggiornare rapidamente eventuali script realizzati in precedenza. Gli aggiornamenti hanno riguardato l'utilizzo del costrutto new (ora obbligatorio), la possibilità di definire proprietà singletons all'interno di moduli e la modifica del comportamento del metodo callSuper(). Per maggiori dettagli rimando alla pagina del sito ufficiale dedicata proprio a questi problemi di compatibilità di versioni (http://jsclass.jcoglan.com/upgrade.html).

La versione 2.0 è una completa riscrittura della versione precedente e presenta notevoli bug-fix soprattutto nella gestione dei moduli e del mixin (personalmente non avevo trovato problemi utilizzandola...) e un miglioramento generale delle performance. Oltre a questi aggiornamenti "dovuti" l'autore ha incluso due nuovi componenti stand-alone (JS.Package e JS.Set) e un nuovo modulo (JS.Stacktrace) da mixare alle nostre classi.

I nuovi componenti della versione 2.0

JS.Stacktrace

JS.Stracktrace è un modulo che può essere mixato nelle nostre classi e presenta funzionalità di debug per quegli sviluppatori che utilizzano all'interno del proprio browser Firefox l'estensione Firebug (personalmente spero che chiunque stia leggendo questo articolo sia come me "dipendente" da Firebug :) ). Esso include infatti alcuni hooks all'interno dei nostri oggetti che interagiscono in maniera non invasiva con la console di Firebug permettendoci di risparmiare molto tempo in fase di debug del codice appena scritto.

JS.Stacktrace stamperà per ogni metodo della classe che include il modulo lo stacktrace indentando in maniera corretta il codice per facilitare la lettura.

Un esempio di stacktrace presente sul sito di JS.Class potrebbe essere:

JS.Observable#addObserver( [function()] )
JS.Observable#addObserver() --> undefined
Child#run( ["something"] )
  Child#fire( [] )
    JS.Observable#notifyObservers( [] )
      JS.Observable#isChanged( [] )
      JS.Observable#isChanged() --> true
      JS.Observable#countObservers( [] )
      JS.Observable#countObservers() --> 1
      Parent#run( [] )
        Parent#getString( [] )
        Parent#getString() --> Running
      Parent#run() --> Running
    JS.Observable#notifyObservers() --> undefined
  Child#fire() --> undefined
  Mixin#run( ["something"] )
    Parent#run( ["something"] )
      Parent#getString( [] )
      Parent#getString() --> Running
    Parent#run() --> Running
  Mixin#run() --> RUNNING
Child#run() --> RUNNING!!!

Oltre a questa comoda funzionalità, il modulo permette anche di definire il livello di logging in console. Di default il livello è "full" e permette di avere in console l'intero stacktrace dei metodi invocati. L'alternativa proposta all'interno da JS.Class è "errors" che stamperà in console solamente le eccezioni lanciate e non "catchate" da nessuno. Per modificare il livello basterà modificare la proprietà statica logLevel:

JS.StackTrace.logLevel = 'errors'

JS.Set

Questo componente, così come il prossimo, rientrano non in maniera diretta in quegli aspetti del framework che permettono di emulare Ruby con Javascript, ma possono comunque essere utili e funzionali.

JS.Set permette di realizzare vettori di oggetti che non possono presentare al loro interno oggetti tra di loro uguali.Questa componente presenta al suo interno anche una specializzazione di JS.Set: JS.SortedSet che presenta le stesse API ma che ha un motore interno di gestione dei dati ottimizzato per quelle liste di oggetti che necessitano un ordine ben preciso.

I metodi esposti da questo oggetto sono:

  • add(item): aggiunge un valore alla lista solo se non è gia presente. Ritorna un booleano che indica se l'oggetto è stato effettivamente inserito o no;
var set = new JS.Set();
set.add(1); //ritorna true
set.add(2); //ritorna true
set.add(1); //ritorna false
  • classify(func, scope): suddivide la collezione in sotto-collezioni più piccole sulla base del valore ritornato dalla funzione passata come parametro;
var set = new Js.Set([1,2,3,4,5,6,7,8,9,0]);
var subsets = set.classify(function(i) {
	return i%2 == 0;
});
/*subset sarà cosi composto
subset[0] = [2,4,6,8,0]
subset[1] = [1,3,5,7,9]*/
  • clear(): rimuove tutti i valori inseriti precendentemente nella lista;
  • complement(array): ritorna un nuovo set composto dai valori di array che non sono presenti nel set;
  • contains(item): ritorna un booleano che indica se item è presente nel set;
  • difference(array): ritorna un nuovo set composto dai valori del set che non sono in array;
  • divide(func, scope): simile a classify, ma ritorna un set contenente altri set;
  • equals(set): ritorna un boolean se i due set contengono gli stessi elementi;
  • flatten(): sposta gli elementi di più set in un unico grande set;
var set1 = new JS.Set([1,2,3]);
var set2 = new JS.Set([2,3,4]);
var s = new JS.Set([set1, set2]);
var flat = s.flatten(); //ritorna un set contenente 1,2,3,4
  • intersection(set): ritorna un set contenente i valori comuni ai due set;

isEmpty(): ritorna true se il set è vuoto;

isProperSubset(set) / isSubset(set): ritorna true se il set sul quale è stato invocato il metodo è un sottoinsieme del set passato come parametro;

isProperSuperset(set) / isSuperset(set): ritorna true se il set passato come parametro è un sotto insieme del set sul quale è stato invocato il metodo;

merge(set): unisce i due set in un unico;

product(set): ritorna un nuovo set contenente tutte le possibili coppie di valori;

rebuilt(): effettua un controllo di integrità sui valori inseriti e nel caso di un JS.SortedSet riordina i valori inseriti;

remove(item): rimuove item dal set;

removeIf(func, scope): esegue la funzione per ciascun item del set e elimina quelli che ricevono true come valore di ritorno della funzione;

replace(array): sostituisce i valori precedenti con quelli passati come parametro;

subtract(array): simile a difference ma in questo caso modifica il set stesso;

union(set): ritorna un nuovo set che contiene tutti gli elementi;

xor(array): ritorna un nuovo set contenente i vettori che ritornano true alla funzione logica xor (i valori presenti solo in una sola lista ma non in entrambe).

JS.Packages

JS.Packages è una classe di utilità che si occupa di caricare a runtime nuovi script on-demand con il supporto ulteriore per eventuali dipendenze da altre librerie.

Una volta definita la struttura di eventuali librerie esterne e delle loro dipendenze (rimando alla documentazione ufficiale per un esempio pratico: http://jsclass.jcoglan.com/packages.html) basta invocare il metodo require per delegare al framework lo scaricamento del file e delle sue dipendenze.


Ti consigliamo anche