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
persona
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
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
privEmail
È 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
Nel seguente esempio, setEmail()
getEmail()
privEmail
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
È comunque possibile simulare il supporto dei membri protetti
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
protectedInfo
codiceInterno
protectedInfo
Il costruttore programmatore()
protectedInfo
protectedInfo
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
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
mostraNomeCompleto()
privNome
privCognome
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
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
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
Polimorfismo
Nella programmazione ad oggetti il polimorfismo è inteso in diversi modi, anche se alla base c'è una nozione comune:
- overloading
- polimorfismo parametrico
- polimorfismo per inclusione
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
- l'associazione
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 |