Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 46 di 112
  • livello avanzato
Indice lezioni

Principi OOP in JavaScript

I pattern di riferimento per implementare in JS i principi basilari della programmazione orientata agli oggetti: Incapsulamento, ereditarietà, polimorfismo, aggregazione, associazione.
I pattern di riferimento per implementare in JS i principi basilari della programmazione orientata agli oggetti: Incapsulamento, ereditarietà, polimorfismo, aggregazione, associazione.
Link copiato negli appunti

JavaScript supporta la programmazione orientata agli oggetti in maniera particolare rispetto ai classici linguaggi di programmazione OOP come Java, C++ o C#. Abbiamo visto ad esempio come non supporti le classi ma preveda ugualmente un meccanismo, i prototipi, che consente di ottenere un risultato analogo e di implementare l'ereditarietà in maniera altrettanto efficace.

La sua natura dinamica e la sua flessibilità non prevedono costrutti predefiniti per il supporto di alcuni dei principi generali della programmazione ad oggetti. Per questo motivo c'è chi sostiene che JavaScript non sia un vero linguaggio orientato agli oggetti. Esistono tuttavia dei pattern di riferimento per implementare questi principi in modo altrettanto efficace di quanto prevedono i linguaggi OOP classici.

In particolare, un linguaggio OOP deve supportare i seguenti principi:

Principio Descrizione
Incapsulamento la capacità di concentrare in un'unica entità (oggetto) dati e funzionalità
Aggregazione la possibilità di un oggetto di avere altri oggetti al suo interno
Ereditarietà la dipendenza delle caratteristiche di un oggetto da una o più definizioni di altri oggetti
Polimorfismo la capacità di gestire oggetti senza conoscerne in anticipo i dettagli
Associazione la possibilità di un oggetto di fare riferimento ad un altro oggetto

Prendiamo quindi in esame questi principi ed analizziamo le problematiche di implementazione in JavaScript.

Incapsulamento

Gli oggetti rappresentano il cardine del modello di programmazione OOP e l'espressione tipica dell'incapsulamento, della capacità cioè di concentrare in un'unica entità dati (proprietà) e funzionalità (metodi). Come abbiamo avuto modo di ripetere più volte, tutto in JavaScript è oggetto: dagli oggetti propriamente detti agli array, dalle funzioni alle stringhe alle espressioni regolari. Da questo punto di vista, quindi, possiamo affermare che l'incapsulamento è un principio pienamente supportato da JavaScript.

Tuttavia l'incapsulamento è spesso collegato ad un principio, l'information hiding, che consiste nel proteggere e controllare le parti di un oggetto accessibili dall'esterno. In altre parole, un oggetto deve avere la capacità di imporre regole di accesso dall'esterno ai propri membri.

La maggior parte dei linguaggi orientati agli oggetti prevedono una classificazione dell'accessibilità dei propri membri in base ad un livello di incapsulamento (encapsulation level):

I membri di un oggetto JavaScript sono tutti pubblici. Ad esempio, i membri dell'oggetto persona definito dal seguente costruttore sono accessibili e modificabili da qualsiasi funzione esterna:

function persona() {
	this.nome = "";
	this.cognome = "";
	this.email = "";
	this.mostraNomeCompleto = function() {...};
}
var marioRossi = new persona();
marioRossi.nome = "Mario";
marioRossi.cognome = "Rossi";
marioRossi.email = "mario.rossi@html.it";

Un tipico membro privato in un oggetto JavaScript è una variabile locale definita nel costruttore:

function persona(nome, cognome) {
	var privNome = nome;
	var privCognome = cognome;
	var privEmail = "";
	this.nome = privNome;
	this.cognome = privCognome;
	this.email = privEmail;
	this.mostraNomeCompleto = function() {
		console.log(this.nome+' '+this.cognome);
	};
}

Le variabili privNome, privCognome e privEmail non sono direttamente accessibili dall'esterno, anche se lo sono tramite le corrispondenti proprietà.

È naturale che una simile mappatura tra variabili private e membri pubblici non ha alcuna vera utilità. Avere membri non direttamente accessibili dall'esterno, ma accessibili mediante un intermediario consente di effettuare ad esempio verifiche di validità dei valori o altre operazioni che non necessariamente devono essere conosciute all'esterno dell'oggetto.

Ad esempio, un oggetto può implementare un controllo di validità dell'indirizzo di e-mail prima di assegnarlo alla sua variabile privata. Questo è il ruolo dei membri privilegiati, cioè di membri pubblici che hanno accesso a membri privati.

Nel seguente esempio, setEmail() e getEmail() sono membri privilegiati che consentono rispettivamente di validare l'indirizzo email da assegnare al membro privato privEmail e di restituire il valore corrente:

function persona(nome, cognome) {
	var privNome = nome;
	var privCognome = cognome;
	var privEmail = "";
	function isValidEmail(value) {
		return true; // è solo per prova, non verifica nulla
	}
	this.nome = privNome;
	this.cognome = privCognome;
	this.getEmail = function() { return privEmail; };
    this.setEmail = function(value) {
				if (isValidEmail(value)) privEmail = value;
	};
	this.mostraNomeCompleto = function() {
				console.log(this.nome+' '+this.cognome+' '+this.getEmail());
	};
}

Purtroppo JavaScript non ha un supporto diretto per i membri protetti, cioè non prevede che un membro sia accessibile soltanto agli oggetti che ereditano le sue caratteristiche.

È comunque possibile simulare il supporto dei membri protetti prevedendo il passaggio di un argomento supplementare nel costruttore della classe base. Questo argomento è un oggetto che funge da repository dei membri protetti, come mostrato nel seguente esempio:

function persona(nome, cognome, protectedInfo) {
	var privNome = nome;
	var privCognome = cognome;
	var privEmail = "";
	protectedInfo =  protectedInfo || {};
	protectedInfo.codiceInterno = "12345ABC";
	function isValidEmail(value) {
		return true; // è solo per prova, non verifica nulla
	}
	this.nome = privNome;
	this.cognome = privCognome;
	this.getEmail = function() {return privEmail;};
	this.setEmail = function(value) {
				if (isValidEmail(value)) privEmail = value;
	};
	this.mostraNomeCompleto = function() {
				console.log(this.nome+' '+this.cognome+' '+this.getEmail());
	};
}
function programmatore(nome, cognome) {
	var protectedInfo = {}; // creiamo una oggetto privato per avere un riferimento
	// facciamo in modo che il valore sia impostato dalla classe padre
	persona.call(this, nome, cognome, protectedInfo);
	this.codice = protectedInfo.codiceInterno;
	this.linguaggiConosciuti = [];
}

Come possiamo vedere, il costruttore dell'oggetto persona prevede l'argomento aggiuntivo protectedInfo. Questo è un oggetto che viene inizializzato, nel caso non venga passato, e a cui viene assegnata la proprietà codiceInterno. Naturalmente l'oggetto protectedInfo non è accessibile dall'esterno.

Il costruttore programmatore(), nell'invocazione del costruttore base, passa un oggetto vuoto a cui verranno assegnati i membri protetti. Ricordiamo infatti che il passaggio di oggetti avviene per riferimento, quindi ogni modifica apportata all'oggetto protectedInfo durante l'esecuzione del costruttore base sarà disponibile al ritorno dell'esecuzione stessa. Il contenuto dell'oggetto protectedInfo sarà quindi pienamente accessibile dal costruttore dell'oggetto derivato.

In conclusione quindi, il supporto dei livelli di incapsulamento è in qualche modo disponibile in JavaScript. L'esistenza di tale supporto si basa essenzialmente sulla relazione esistente tra membri privati e membri pubblici e sulla disponibilità della closure che consente ad un metodo di accedere ai membri privati del costruttore anche dopo che è terminata l'esecuzione del costruttore stesso.

Tutto ciò non è purtroppo valido quando proviamo a combinare l'incapsulamento con l'ereditarietà basata sui prototipi. Infatti un metodo assegnato al prototipo di un costruttore non ha accesso ai membri interni del costruttore stesso:

function persona(nome, cognome) {
	var privNome = nome;
	var privCognome = cognome;
	this.nome = privNome;
	this.cognome = privCognome;
}
persona.prototype.mostraNomeCompleto = function() {return privNome + privCognome};

In questo esempio, se proviamo a creare un oggetto persona e ad invocare il metodo mostraNomeCompleto() otterremo un errore, dal momento che le due variabili privNome e privCognome non sono accessibili. Ne consegue quindi che nella progettazione di un oggetto che può essere derivato occorre trovare un compromesso tra livello di incapsulamento ed ereditarietà.

Aggregazione

L'aggregazione è la capacità di un oggetto di contenere altri oggetti. Come abbiamo avuto modo di vedere nel corso della guida, JavaScript supporta senza particolari problemi questa caratteristica.

Ad esempio, la seguente definizione di una persona include la proprietà indirizzo come un oggetto:

var marioRossi = {
	nome: "Mario",
	cognome: "Rossi",
	indirizzo: {
		via: "Via Garibaldi",
		numero: "11",
		comune: "Roma",
		provincia: "RM"
	}
};

Il risultato può essere visto come un oggetto generato dall'aggregazione di due oggetti. Naturalmente è possibile che l'oggetto indirizzo sia a sua volta il risultato di un'aggregazione e così via consentendo la creazione di gerarchie di oggetti.

Ereditarietà

Abbiamo visto come l'ereditarietà di JavaScript si basi su un modello diverso da quello dei classici linguaggi di programmazione orientata agli oggetti. Nonostante l'assenza di classi, il meccanismo dei prototipi consente di ottenere un nuovo oggetto le cui caratteristiche derivano, in tutto o in parte, da un altro oggetto in maniera altrettanto efficace.

Come abbiamo visto ci sono diversi modi per derivare un oggetto da un altro. Possiamo ad esempio creare un oggetto derivato da un altro impostando opportunamente il suo prototipo. Ad esempio, facendo riferimento al costruttore dell'oggetto persona, possiamo definire il costruttore dell'oggetto programmatore nel seguente modo:

function programmatore() {
	this.linguaggiConosciuti = [];
}
programmatore.prototype = new persona();
var marioRossi = new programmatore();

Impostando il prototipo del costruttore programmatore come istanza del costruttore persona, abbiamo di fatto ereditato le caratteristiche di persona.

In alternativa possiamo invocare il costruttore di persona all'interno del costruttore di programmatore:

function programmatore() {
	persona.call(this);
	this.linguaggiConosciuti = [];
}
var marioRossi = new programmatore();

JavaScript non supporta l'ereditarietà multipla, cioè la possibilità di creare un oggetto che eredita le caratteristiche di due o più oggetti.

Polimorfismo

Nella programmazione ad oggetti il polimorfismo è inteso in diversi modi, anche se alla base c'è una nozione comune:

  • overloading, la possibilità di prevedere metodi che manipolano tipi di dato diversi;
  • polimorfismo parametrico, la possibilità di prevedere tipi generici, non conosciuti a priori;
  • polimorfismo per inclusione, la possibilità di avere espressioni il cui tipo può essere rappresentato da una classe e dalle classi da essa derivate.

Tutti e tre i concetti risultano rilevanti linguaggi fortemente tipizzati come C++, Java o C#, ma per un linguaggio dinamico come JavaScript questi concetti risultano intrinsecamente supportati.

Per un linguaggio a tipizzazione dinamica, risulta implicito poter lavorare con tipi generici e gestire senza problemi oggetti di tipo diverso. Risulta altrettanto immediato il fatto che un metodo JavaScript supporti l'overloading manipolando tipi di dato diversi.

Tra l'altro, a differenza dei linguaggi fortemente tipizzati, non è necessario avere due o più definizioni diverse in base al tipo. Ad esempio, mentre in C# dobbiamo ricorrere ad una definizione del genere:

public string Add(int x, int y)
{
	return x + y;
}
public string Add(string x, string y)
{
	return x + y;
}

in JavaScript definiamo un solo metodo:

function Add(x, y) {
	return x + y;
}

Associazione

L'associazione non è in genere ritenuta un requisito fondamentale per poter definire un linguaggio orientato agli oggetti. Tuttavia è opportuno prenderla in considerazione visto il frequente utilizzo che si fa di essa. In pratica, l'associazione è il principio in base al quale un oggetto viene messo in relazione con un altro oggetto. Ad esempio, per definire una relazione genitore-figlio tra persone possiamo farlo nel seguente modo:

function persona(nome, cognome) {
	this.nome = nome;
	this.cognome = cognome;
	this.genitore = null;
}
var marioRossi = new persona("Mario", "Rossi");
var giuseppeRossi = new persona("Giuseppe", "Rossi");
marioRossi.genitore = giuseppeRossi;

L'assegnazione dell'oggetto giuseppeRossi alla proprietà gentiore di marioRossi definisce l'associazione tra i due oggetti.

È importante non confondere l'associazione con l'aggregazione. Anche se il supporto dei due principi è sintatticamente identico, l'assegnamento di un oggetto ad una proprietà, da un punto di vista concettuale rappresentano situazioni diverse.

  • l'aggregazione è il principio che consente di creare un oggetto composto da più oggetti;
  • l'associazione mette in relazione oggetti autonomi.

Inoltre, mentre l'aggregazione non prevede che un oggetto faccia parte di oggetti diversi, l'associazione prevede che un oggetto possa essere associato a più oggetti, come nel seguente esempio:

marioRossi.genitore  =  giuseppeRossi;
giuliaRossi.genitore =  giuseppeRossi;
marcoRossi.genitore  =  giuseppeRossi;

In ogni caso, JavaScript non fa alcun controllo sul modo in cui associamo gli oggetti tra di loro. L'associazione pone quindi più un vincolo concettuale che tecnico.

Ti consigliamo anche

Livello di incapsulamento Descrizione
pubblico accessibile a tutti
privato accessibile solo all'interno dell'oggetto
privilegiato accessibile da tutti e che consente un accesso indiretto ai membri privati
protetto accessibile dall'interno dell'oggetto e dagli oggetti derivati