L’abbondanza di nuovo materiale reperibile in rete in tema di “modularità” delle applicazioni Javascript, tipicamente per quanto riguarda applicazioni web particolarmente“client-side” (o addirittura SPA) ma anche per quanto concerne applicazioni NodeJs, unitamente ad iniziative quali TypeScript, dimostrano il bisogno che la moderna comunità degli sviluppatori ha di metodologie e best practices finalizzate alla riduzione del rischio di “spaghetti-code” nella creazione di progetti Javascript complessi, quali quelli che quotidianamente vengono affrontati da chi utilizza questo linguaggio per professione.
Senza addentrarci in temi troppo “accademici”, credo non si possa negare che gli elementi più critici in tal senso sono due:
Quanto al primo punto, in realtà non si può dire che Javascript non sia un linguaggio object-oriented, anzi al contrario lo è, ed addirittura molto più di altri (anche le funzioni sono oggetti, ad esempio); quello che con più esattezza motiva la scarsa propensione ad essere utilizzato come linguaggio adatto all’OOP è semmai la sua peculiarità ad essere più “object-oriented” che “class-oriented” (o “type-oriented”), cosa che di fatto mette a disagio chi ha più dimestichezza con linguaggi quali C++, Java e C#, caratterizzati da un paradigma effettivamente molto differente.
In merito al secondo punto, se facciamo ad esempio riferimento ad una applicazione web più o meno tipica, non possiamo non notare come l’inclusione di 10-15 script “*.js” fatta da parte dei vari frammenti di markup HTML che popolano ciascuna pagina/vista dell’applicazione ha l’effetto di “inquinare” (non a caso si parla di “namespace pollution”) lo spazio in cui operano gli oggetti attivi all’interno dell’engine (che sia quello interno al browser o quello definito in nodejs) che sta eseguendo la nostra applicazione, senza considerare l’intricato rapporto di dipendenze che molto spesso esiste tra gli script in questione.
Sebbene non mi senta di dire che si tratti della soluzione “ottima” o “definitiva”, ho trovato particolarmente efficace la soluzione illustrata di seguito, in cui il primo punto critico è affrontato utilizzando un pattern per la traduzione dei concetti tipici di un linguaggio OOP quale Java, C++ o C#, mentre il secondo viene sostanzialmente risolto mediante l’adozione del cosiddetto “Module-pattern” (nella “accezione” AMD, più precisamente), reso per l’occasione meno “complicato” dall’utilizzo del framework RequireJS, che sta di fatto diventando uno standard infrastrutturale anche in molti framework più “verticali”.
Per rendere l’illustrazione di questo approccio più eloquente possibile, direi di utilizzare l’esempio che segue, costituito dalla seguente struttura di file:
Il file default.htm, un po’ scarso nella sua espressione puramente UI dal momento che tutto l’output del demo si svolge all’interno della console del browser, è il seguente, in cui è evidenziata la parte di dichiarazione delle dipendenze gestita da RequireJS:
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>RequireJS + OOP demo</title> | |
<!-- This is a special version of jQuery with RequireJS built-in --> | |
<script data-main="scripts/main" src="scripts/require.js"></script> | |
</head> | |
<body> | |
<h1>RequireJS + OOP demo</h1> | |
<p>L'output di questo esempio è tutto nella console!</p> | |
</body> | |
</html> |
Lo script person.js è il seguente:
// "define" è una funzione di RequireJS deputata alla dichiarazione | |
// di un modulo. Il parametro passato rappresenta la funzione invocata da | |
// chi dipenderà da questo modulo in fase di risoluzione delle dipendenze. | |
// Il valore di ritorno sarà passato (da RequireJS) al "richiedente". | |
define(function() { | |
// Questo modulo "esporta" un oggetto che ha lo scopo | |
// di dichiarare la definizione di una "classe" Person | |
// Questo è un costruttore della "classe" Person | |
var Person=function() | |
{ | |
// questo è un field privato | |
var _age=18; | |
// questo è un metodo "privilegiato", ossia chiamabile | |
// pubblicamente ma che ha diritto di accesso a field privati | |
this.setAge=function(newval) { | |
_age=newval; | |
}; | |
// questo è un altro metodo "privilegiato" | |
this.sayAge=function() { | |
console.info(_age); | |
}; | |
// questa è una proprietà pubblica, inizializzata | |
this.name="(default name)"; | |
}; | |
// Questa è una proprietà statica | |
Person.s_Description="Persona"; | |
// Questo è un metodo pubblico non "privilegiato" | |
Person.prototype.walk = function(){ | |
console.info ('I am walking!'); | |
}; | |
// Idem questo (che è "overridato" sulla classe Student derivata) | |
Person.prototype.sayHello = function(){ | |
console.info ('hello'); | |
}; | |
// metodo pubblico che accede ad una proprietà | |
Person.prototype.sayName=function() { | |
console.info(this.name); | |
}; | |
// "esporto" la definizione di Person | |
return Person; | |
}); |
Mentre Student.js è il seguente:
// il modulo definito di seguito utilizza anche l'array di stringhe | |
// come primo parametro ad indicare che il modulo in questione dipende | |
// a sua volta dal modulo "person". In questo modo RequireJS caricherà | |
// e "valuterà" lo script person.js prima di eseguire la funzione di | |
// definizione del modulo corrente | |
define(['person'],function(Person) { | |
// definiamo qui la "classe" Student, derivata da Person | |
// definiamo un costruttore (niente overload, però!) | |
var Student=function(city) { | |
// definisco una proprietà | |
this.school=null; | |
// e un field privato, inizializzato con il parametro del ctor | |
var _city=city; | |
// metodo privilegiato, può accedere a "_city" | |
this.saySchoolAndCity = function(){ | |
console.info(this.school + ' in ' + _city); | |
} | |
}; | |
// Student EREDITA da Person | |
Student.prototype = new Person(); | |
// correggiamo il "puntamento" del costruttore di Student, | |
// che altrimenti per default punta a quello di Student | |
Student.prototype.constructor = Student; | |
// facciamo l'override del metodo "sayhello()" | |
Student.prototype.sayHello = function(){ | |
console.info('hi, I am a student'); | |
} | |
// aggiungiamo il metodo "sayGoodBye()" | |
Student.prototype.sayGoodBye = function(){ | |
console.info('goodBye'); | |
} | |
// ed esportiamo questa definizione di Student | |
return Student; | |
}); |
Per verificare il funzionamento del tutto possiamo utilizzare un main.js come quello che segue:
// questo script dipende da "person.js" e da "student.js", che verranno | |
// caricati e valutati da RequireJS, il quale eseguirà la funzione passata | |
// come secondo parametro al termine della risoluzione del grafo delle dipendenze | |
require(["person","student"], function(personModule,studentModule) { | |
//qui person.js e student.js sono stati caricati, lanciamo il "main" | |
runMain(personModule,studentModule); | |
}); | |
// Questa funzione rappresenta l'entry-point della nostra applicazione | |
function runMain(Person,Student) { | |
// Istanziamo due persone | |
var person1=new Person(); | |
var person2=new Person(); | |
// Invochiamo dei metodi (pubblici o privilegiati) | |
person1.sayName(); | |
person1.sayAge(); | |
// Settiamo una proprietà | |
person1.name="Rick"; | |
// Proviamo a settare un field privato (nessuna eccezione, ma non ha effetto!) | |
person1._age=19; | |
// Verifichiamo lo stato di "person1" | |
person1.sayName(); | |
person1.sayAge(); | |
// Eseguiamo il metodo privilegiato e riverifichiamo che "_age" sia cambiato | |
person1.setAge(20); | |
person1.sayAge(); | |
// Verifichiamo che l'altra istanza ha il suo stato distinto da "person1" | |
person2.sayName(); | |
person2.sayAge(); | |
// Proprietà statica, esposta solo da Person, non dalle sue istanze | |
console.info(Person.s_Description); | |
console.info(person1.s_Description); | |
// Istanziamo la classe Student, nel primo caso senza passare parametri al ctor | |
var student1 = new Student(); | |
var student2 = new Student("Camerino"); | |
// settiamo una proprietà erediatata | |
student1.name="Jeffrey"; | |
// verifichiamo l'override | |
student1.sayHello(); | |
// verifichiamo i metodi della classe base e l'accesso ai field della classe base | |
student1.sayName(); | |
student1.walk(); | |
// verifichiamo che il ctor ha inizializzato (se eseguito con parametri) | |
// correttamente field o proprietà | |
student2.school="MIT"; | |
student2.saySchoolAndCity(); | |
student1.saySchoolAndCity(); | |
// eseguiamo un metodo definito solo in Student | |
student1.sayGoodBye(); | |
// verifichiamo che le istanze hanno stato distinto | |
student2.name="Henry"; | |
console.info(student1.name); | |
console.info(student2.name); | |
// verifichiamo la "tipizzazione" | |
console.info(student1 instanceof Person); | |
console.info(student1 instanceof Student); | |
console.info(person1 instanceof Person); | |
console.info(person2 instanceof Student); | |
} |
Così facendo, dovremmo ottenere un output di questo tipo:
default name) person.js:46 | |
18 person.js:24 | |
Rick person.js:46 | |
18 person.js:24 | |
20 person.js:24 | |
(default name) person.js:46 | |
18 person.js:24 | |
Persona main.js:39 | |
undefined main.js:40 | |
hi, I am a student student.js:34 | |
Jeffrey person.js:46 | |
I am walking! person.js:36 | |
MIT in Camerino student.js:21 | |
null in undefined student.js:21 | |
goodBye student.js:39 | |
Jeffrey main.js:68 | |
Henry main.js:69 | |
true main.js:72 | |
true main.js:73 | |
true main.js:74 | |
false main.js:75 |
Per approfondimenti:
https://developer.mozilla.org/en-US/docs/JavaScript/Introduction_to_Object-Oriented_JavaScript
http://phrogz.net/JS/classes/OOPinJS.html
http://phrogz.net/JS/classes/OOPinJS2.html
http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth
l’abbondanza di strumenti di modularità JS mostra il bisogno che la moderna comunità degli sviluppatori ha di metodologie e best practices finalizzate alla riduzione del rischio di “spaghetti-code” nella creazione di progetti Javascript complessi