Javascript OOP con AMD e RequireJS

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:

  • Scarsa propensione all’OOP
  • Difficoltà nella gestione delle dipendenze

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:

  • default.htm
  • scripts (dir)
    • scripts/main.js
    • scripts/person.js
    • scripts/student.js
    • scripts/require.js

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>
view raw gistfile1.txt hosted with ❤ by GitHub

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;
});
view raw gistfile1.js hosted with ❤ by GitHub

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;
});
view raw gistfile1.js hosted with ❤ by GitHub

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);
}
view raw gistfile1.js hosted with ❤ by GitHub

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
view raw gistfile1.txt hosted with ❤ by GitHub

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

https://github.com/amdjs/amdjs-api/wiki/AMD

http://requirejs.org/docs/start.html

29/07/2014
Tags: , , ,
Categorie: PostSviluppo
Autore: Lorenzo Maiorfi

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


Support