La programmazione ad oggetti ci offre la possibilità di definire un set di classi “base” da estendere di volta in volta per creare oggetti più complessi senza dover riscrivere ogni volta l’inizializzazione di proprietà condivise.

Riprendiamo le classi di esempio definite nell’articolo Programmazione ad oggetti con Function – Introduzione.

function Employee (params) {
  var params = params || {};
  this.id = params.id;
  this.name = params.name;
  this.company = params.company;
  this.role = params.role;
  this.manager = params.manager;
}
function Manager (params) {
  var params = params || {};
  this.id = params.id;
  this.name = params.name;
  this.company = params.company;
  this.employees = params.employees;
  this.project = params.project;
}

Nelle due classe così definite ci sono delle inizializzazioni che abbiamo dovuto riscrivere ma avremmo potuto evitare. Per questo motivo creiamo ora la classe Person che poi estenderemo all’interno di Employee e Manager.

function Person (params) {
  var params = params || {};
  this.id = params.id;
  this.name = params.name;
  this.company = params.company;
}

Per estendere questa classe per esempio dentro Employee potremo istanziare una variabile Person al suo interno, aggiungerci altre proprietà e metodi, infine ritornare l’istanza così estesa.

function Employee (params) {
  var params = params || {};
  var person = new Person(params);
  person.role = params.role;
  person.manager = params.manager;
  return person;
}

In questa maniera però pur ottenendo all’atto pratico ciò che vogliamo, cioè una istanza completa con le proprietà definite in Person e Employee, non stiamo definendo correttamente un’istanza di Employee.

var e = new Employee();
e instanceof Employee // false
e instanceof Person // true

Definire correttamente una variabile d’istanza può non sembrare importante perchè tutto sommato la variabile e comunque ha tutto quello di cui si ha bisogno, ma diventa cruciale se volessimo creare ad esempio una funzione che a seconda del tipo dell’istanza passata in input esegue va ad eseguire operazioni differenti.

function classFactory (input) {
  if (input instanceof Person) {
    ...
  } else if (input instanceof Employee) {
    ...
  }
  ...
}

Per risolvere questo problema, la classe nativa Function di JavaScript ci offre due metodi: callapply. Entrambi servono allo scopo di applicare al contesto specificato i valori passati in input senza cambiarne la natura, ma mentre il primo accetta singoli parametri, il secondo si aspetta di leggerli da un array.

function Employee (params) {
  var params = params || {};
  Person.call(this, params); // Person.apply(this, [params])
  this.role = params.role;
  this.manager = params.manager;
}

Come possiamo notare non abbiamo più bisogno di definire una variabile interna di appoggio da ritornare come abbiamo fatto prima, in quanto la funzione call (o apply) si occuperà prendere i valori contenuti dentro params, applicarli alla classe Person e mantenere this legata al contesto dell’istanza di Employee.

var e = new Employee();
e instanceof Employee // true
e instanceof Person // false

Il processo di estensione di oggetti può essere concatenato liberamente: è possible estendere classi che a loro volta estendono altre classi. Ad esempio se ridefinissimo la classe Manager con le stesse proprietà di Employee e in più quelle di project e employees, allora avremo questa situazione

function Manager (params) {
  ...
  Employee.call(this, params);
  ...
}
function Employee (params) {
  ...
  Person.call(this, params);
  ...
}
function Person (params) {
  ...
}

Inoltre possiamo estendere più classi contemporaneamente all’interno di una sola.

Supponiamo di avere a disposizione una classe per gestire le credenziali di un utente.

function UserCredential (params) {
  var params = params || {};
  this.user = params.user;
  this.password = this.password;
  this.resetUser = function () {...};
  this.resetPassword = function () {...};
}

Potremmo aggiornare la nostra classe Employee in questo modo

function Employee (params) {
  ...
  Person.call(this, params);
  UserCredential(this, params);
  ...
}

Da tenere bene a mente che se le classi che andiamo ad estendere offrono delle stesse proprietà allora l’ultima in ordine di linea di codice va a sovrascrivere le precedenti. Nel nostro caso UserCredential potrebbe sovrascrivere eventuali proprietà di Person.


Estendere una classe non significa che abbiamo solo la possibilità di sfruttarne variabili e metodi, ma si ha la possibilità anche di sovrascriverne il comportamento a piacimento. Possiamo infatti vedere la classe estesa come un insieme di valori di default, in modo che non sia sempre necessario passare tutte le informazioni per valorizzare le proprietà di una classe.

Ad esempio la classe Employee potrebbe sovrascrivere l’inizializzazione della proprietà id rispetto a Person

function Employee (params) {
  var params = params || {};
  Person.call(this, params); // Person.apply(this, [params]);
  this.id = Math.random();
  this.role = params.role;
  this.manager = params.manager;
}

In questo modo i nostri dipendenti avranno sempre un id valorizzato random pur passandone il valore.

var e1 = new Employee();
var e2 = new Employee({id: 0});

e1.id // Math.random()
e2.id // Math.random();

 

Le funzioni in JavaScript offrono la possibilità di definire ed eseguire operazioni che variano dalla semplice esecuzione di una sequenza di algoritmi, alla gestione di chiamate AJAX o eventi del DOM.

Vediamo ora come poterle applicare alla metodologia della programmazione orientata ad oggetti.

Il cuore del ragionamento ruota intorno alla parola chiave di istanza di una funzione, che ne rappresenta l’esecuzione contestualizzata a particolari valori. Se ad esempio generassimo due istanze di una funzione (o classe) Persona, ognuna di loro avrà la sua proprietà nome, cognome, ecc… indipendentemente valorizzato.


Supponiamo ora di voler gestire una lista di semplici aziende e per ognuna specificare le informazioni sulla sede, dipendenti, manager e quant’altro. Definiremo quindi delle funzioni, che prenderanno il nome più appropriato di classi, che ci daranno gli strumenti per stabilire cosa è un dipendente, un manager, ecc… e quali operazioni si possono fare su di loro.

Scriveremo le funzioni in maiuscolo per far capire che sono la definizione di classi e non metodi da usare con un particolare oggetto o evento.

function Employee (params) {
  var params = params || {};
  this.id = params.id;
  this.name = params.name;
  this.company = params.company;
  this.role = params.role;
  this.manager = params.manager;
}
function Manager (params) {
  var params = params || {};
  this.id = params.id;
  this.name = params.name;
  this.company = params.company;
  this.employees = params.employees;
  this.project = params.project;
}

La prima cosa che si può notare è l’uso della parola chiave this, che serve a specificare che quella variabile di seguito al punto è legata all’istanza della funzione che poi genereremo, e della variabile params che ci serve solo come appoggio per definire un valore di default.

Per poter ora usare queste funzioni come classi e quindi istanziarne oggetti, abbiamo a disosizione la parola chiave new.

var e1 = new Employee({
  name: "John",
  role: "developer",
  company: "Blu Note"
});
var e2 = new Employee();
var m1 = new Manager({
  name: "Riccardo",
  employees: [e1],
  project: "Awesome site"
});

Per poter leggere e scrivere poi le proprietà di ogni istanza basterà usare la notazione puntuale come se fossero dei JSON

e1.name // "John"
e1.role // "developer"

e2.name // undefined

m1.project // "Awesome site"

Possiamo inoltre controllare se una certa variabile sia una istanza di una determinata classe in questa maniera

e1 instanceof Employee // true
e2 instanceof Employee // true
m1 instanceof Employee // false

Proviamo ora ad aggiungere alla classe Employee la gestione del salario, aggiungendo una nuova proprietà salary e un metodo per aggiungere gli aumenti.

function Employee (params) {
  var params = params || {};
  this.id = params.id;
  this.name = params.name;
  this.company = params.company;
  this.role = params.role;
  this.manager = params.manager;
  this.salary = params.salary;
  this.addToSalary = function (value) {
    this.salary = this.salary? this.salary + value: value;
    return this.salary
  }
}

All’interno della funzione addToSalary possiamo accedere ai valori delle proprietà di classe, in questo caso a salary, dato che non varia il contesto del chiamante della funzione add rispetto all’istanza.


 

Gli oggetti JSON offrono un ottimo strumento per organizzare e mappare dati. Li usiamo per inviare informazioni al server con le chiamate AJAX, per rappresentare strutture dati, o anche semplicemente per rendere il codice più chiaro e leggibile. Grazie alla loro flessibilità e facilità di manipolazione possiamo inoltre introdurli all’interno del paradigma di programmazione orientata agli oggetti.

Supponiamo di voler gestire una lista di semplici aziende e per ognuna specificare le informazioni sulla sede, dipendenti, manager e quant’altro. Abbiamo quindi bisogno di definire degli oggetti, che prenderanno il nome più appropriato di classi, che ci daranno gli strumenti per stabilire cosa è un dipendente, un manager, ecc… e quali operazioni si possono fare su di loro.


Cominciamo con il modellare quindi le strutture dati per rappresentare i vari elementi tramite oggetti JSON con valori vuoti di default.

Identificheremo un dipendente come una persona con un ID identificativo, un nome, un ruolo lavorativo, chi è il suo capo e il nome dell’azienda per cui lavora.

Scriveremo le variabili in maiuscolo per far capire che servono a definire classi e proprietà degli elementi e non tanto per manipolare le informazioni.

var Employee = {
  id: 0,
  name: "",
  company: "",
  role: "",
  manager: ""
}

Il modello dati del manager sarà simile a quello del dipendente ma invece che definire un ruolo e chi è il suo manager di riferimento avrà le informazioni su quali siano i suoi dipendenti e quale progetto coordina.

var Manager = {
  id: 0,
  name: "",
  company: "",
  employees: [],
  project: ""
}

Ora potremmo creare il nostro primo dipendente e il nostro primo manager dell’azienda.

var firstEm = Employee;
firstEm.name = "John";
firstEm.role = "developer";
firstEm.company = "Blu Note";
...

var firstMa = Manager;
firstMa.name = "Riccardo";
firstMa.project = "Awesome Site";
....

Purtroppo in questa maniera quando andremo a definire nuove variabili con diversi valori, andremo anche a sovrascrivere quelle precedentemente create. Per evitare questo problema dovremo modificare le classi Person e Manager aggiungendo un metodo da invocare alla creazione di un nuovo elemento e che ci restituisca ogni volta un nuovo oggetto con cui lavorare.

var Employee = {
  create: function (params) {
    var params = params || {};
    return {
      id: params.id,
      name: params.name,
      company: params.company,
      role: params.role,
      manager: params.manager
    }
  }
}
var Manager = {
  create: function (params) {
    var params = params || {};
    return {
      id: 0,
      name: "",
      company: "",
      employees: [],
      project: ""
    }
  }
}

Ora siamo sicuri che invocando il metodo create delle rispettive classi otterremo un nuovo oggetto valorizzato con i parametri passati in input oppure un oggetto JSON vuoto.

var e1 = Employee.create({
  name: "John",
  role: "developer",
  company: "Blu Note"
});

var m1 = Manager.create({
  name: "Riccardo",
  employees: [e1],
  project: "Awesome site"
});

Potremo migliorare ulteriormente l’esempio incapsulando le informazioni condivise tra Employee e Manager in un oggetto più generico che estenderemo per evitare di dover riscrivere inutilmente le proprietà.

Possiamo definire una classe Person che useremo come classe di supporto per inizializzare le informazioni di base sia dei dipendenti che manager con un suo metodo create.

var Person = {
  create: function (params) {
    params = params || {};
    return {
      id: params.id,
      name: params.name,
      company: params.company
    }
  }
}

La classe Employee  estenderà la classe Person ereditandone tutte le caratteristiche e in più setterà valori aggiuntivi necessari a specificare le caratteristiche di un dipendente.

var Employee = {
 create: function (params) {
    var person = Person.create(params);
    person.role = params.role;
    person.manager = params.manager;
    return person;
  }
}

La classe Manager, come la classe Employee, estenderà Person e setterà i suoi valori aggiuntivi.

var Manager = {
  create: function (params) {
    var person = Person.create(params);
    person.project = params.project;
    person.eployees = params.employees;
    return person;
  }
}

In questa maniera potremo creare dipendenti e manager semplicemente invocando il metodo create relativamente di Employee e Manager.

var e1 = Employee.create({
  name: "John",
  role: "developer",
  company: "Blu Note"
});

var e2 = Employee.create({
  name: "Smith",
  role: "designer",
  manager: "Riccardo"
});

var e3 = Employee.create();

var m1 = Manager.create({
  name: "Riccardo",
  employees: [e1, e2],
  project: "Awesome site"
});

Importante da tenere in considerazione che con questa struttura al momento della creazione di un elemento non è necessario passare tutti i valori perchè la classe Person comunque ha il compito di ritornare un oggetto JSON che potrà essere manipolato successivamente.


Supponiamo ora di voler avere a disposizione un metodo copy per facilitare le operazioni di creazione di elementi.

Basterà modificare opportunamente la classe Person per ereditarne “gratis” la funzionalità sia in Employee che in Manager.

var Person = {
  create: function (params) {
    params = params || {};
    return {
      id: params.id,
      name: params.name,
      company: params.company,
      copy: function(opt){
        var opt = opt || {};
        var person = this;
        var clone = {};
        for (var prop in person) {
          if (opt[prop]) {
            clone[prop] = opt[prop]
          } else if (person.hasOwnProperty(prop)) {
            clone[prop] = person[prop]
          }
        }
        return clone
      }
    }
  }
}

In questa maniera potremo invocare il metodo copy direttamente dalla variabile creata per ottenere un nuovo oggetto con gli stessi valori, a meno di quelli che gli passiamo in input.

var e4 = e1.copy();
var e5 = e1.copy({name: "Karl"})

e1.name // "John"
e4.name // "John"
e5.name // "Karl"

La porola chiave this ci permette infatti di contestualizzare la lettura dei valori all’oggetto dal quale invochiamo il metodo copy.

In alternativa avremmo potuto definire il metodo copy a livello di classe e quindi ad esempio dentro Manager, in modo che vada a copiare le chiavi di un primo oggetto passato in input e al più sovrascriverle con un secondo parametro.

var m2 = Manager.copy(m1);
var m3 = Manager.copy(m1, {name: "Smith"})

m1.name // "Riccardo"
m2.name // "Riccardo"
m3.name // "Smith"

Le variabili

Quando le informazioni che dobbiamo salvare, modificare o anche semplicemente leggere non sono solo semplici valori fini a se stessi (come un contatore, il risultato di una somma o un elemento HTML magari da inserire nel DOM), ma valori che per qualche logica rappresentano varie proprietà di un unico elemento, non possiamo affidarci solo alla definizione di singole variabili.

Infatti in questa maniera saremo costretti ad usare nomi i più parlanti possibile per differenziarli e mantenere una buona leggibilità del codice, ma che così facendo rischieremo di dover inventare variabili lunghe e fantasiose anche solo per evitare di avere errori di sovrascrittura.

var mySquareBase = 5;
var mySquarePerimeter = mySquareBase * 4;
var mySquareArea = mySquareBase * mySquareBase;

var yourSquareSide = 8;
var yourSquarePerim = yourSquareSide * 4;
var yourSquareArea = yourSquareSide * yourSquareSide;

In questo esempio sopra riportato (ovviamente enfatizzato ma comunque da una buona idea del problema) si può vedere come abbiamo dovuto definire tre variabili per rappresentare tre semplici proprietà di un quadrato e che per differenziarle da una figura all’altra abbiamo dovuto riscriverle tutte. Inoltre le variabili in questo modo sono legate ad una singola geometria solo perchè il nome ce lo suggerisce e non per qualche regola JavaScript particolare.

Per unire le informazioni sia concettualmente che programmaticamente possiamo usare un oggetto JSON che ci permetterà di definire un modello dati condiviso e una sorta di dizionario unico. Ad esempio i quadrati descritti sopra potranno essere riscritti nel seguente modo

var mySquare = { base: 5, perimeter: function(){ return this.base * 4}, area: function(){ return this.base * this.base} }
var yourSquare = { base: 8, perimeter: function(){ return this.base * 4}, area: function(){ return this.base * this.base} }

mySquare.base // 5
mySquare.perimeter() // 20

yourSquare.base // 8
yourSquare.perimeter() // 20

Questa nuovo modo di definire le due variabili potrebbe sembrare uno step complesso e macchinoso, ma in questa maniera ora le informazioni di base, perimetro ed area possono essere considerati a tutti gli effetti proprietà di un quadrato e che in pochi semplici passaggi possono essere astratte e generalizzate.

Supponiamo infatti di avere nelle nostre librerie un metodo copia del tipo

function clone (obj) {
    if (null == obj || "object" != typeof obj) return obj;
    var copy = obj.constructor();
    for (var attr in obj) {
        if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr];
    }
    return copy;
}

potremo anche migliorare la fase di inizializzazione delle figure in questa maniera

var square = { base: 0, perimetro: function(){ return this.base * 4}, area: function(){ return this.base * this.base} };

var mySquare = clone(square);
mySquare.base = 5;

var yourSquare = clone(square);
yourSquare.base = 8;

 

 


L’uso di oggetti JSON per definire un modello dati semplice o complesso che sia, ci porta una serie di vantaggi

  • legare diverse informazioni sia logicamente che via codice sotto un unico contenitore per avere una migliore rappresentazione del dato
  • definire una nomenclatura chiara e indipendente dal nome della variabile a cui associamo il JSON riducendo molto il rischio di sovrascrittura
  • condividere una struttura dati il più astratta possibile che definita una volta sola e beneficiare automaticamente di eventuali modifiche alla stessa in tutte le sue implementazioni

Le funzioni

Un’altro importante impiego dei JSON per migliore la leggibilità e qualità del codice riguarda la definizione delle funzioni.

Man mano che i nostri progetti crescono e vengono aggiunte funzionalità dobbiamo splittare la nostra libreria JavaScript dovrà essere suddivisa in più file per evitare di avere un’unica grande lista di funzioni anche nelle pagine in qui non è necessario (con un conseguente spreco di banda e ritardo nel caricamento della pagina).

Inoltre, come per le variabili, il rischio di definire delle funzioni con lo stesso nome è molto alto e sarebbe un vero spreco di tempo se ogni volta che definiamo una funzione dovessimo andare a controllare se nei vari file del progetto ci sia già  definita un metodo con lo stesso nome.

Come per le variabili, “impacchettando” i metodi dentro vari JSON e usando dei nomi generici potremo quindi definire una nomenclatura condivisibile e più semplice pur mantenendola parlante.

Ad esempio supponiamo di voler definire dei widget html da poter riusare a piacimento all’interno delle nostre pagine web. Ogni componente avrà sicuramente delle specifiche ben distinte l’une dalle altre, ma potremmo comunque definire due componenti in questa maniera:

var firstComponent = {
  initialize: function () {
    // Intialize component data
    this.initGlobals(); // Initialize global variables for this current component
    this.loadImages();  // Load images of this current component
    ...
  },
  initGlobals: function () {
    // Define some variables to use them around the component
    ...
  }
  loadImages: function () {
    // If there are lazy images load them
    ...
  }
  // Other methods
  ...
}

var secondComponent = {
  initialize: function () {...},
  this.initGlobals: function () {...},
  this.loadImages: function () {...}
  ...
}

Andando a chiamare le funzioni dei rispettivi componenti sarà molto semplice capire cosa stiamo andando ad invocare.

firstComponent.initialize();
secondComponent.initialize();

Potremo ora sfruttare questa nomenclatura condivisa per creare dei metodi che automaticamente avviano i componenti a partire da una lista di quelli che vogliamo.

var components = [firstComponent, secondComponent, ...];
...
function initComponents (list) {
  for (var index in list) {
    var component = list[index];
    component.initialize();
  }
}
...
initComponents(components);

 


Imparare a scrivere codice secondo queste regole ci faciliterà infine la comprensione e il passaggio ad una programmazione orientata ad oggetti in JavaScript, determinate per la realizzazione di librerie più chiare e mantenibili nel tempo.