Incapsulamento JavaScript

L’incapsulamento è uno dei principi fondamentali della programmazione orientata agli oggetti che consiste nel nascondere i dettagli interni di implementazione e esporre solo un’interfaccia pubblica ben definita. In JavaScript, anche se tradizionalmente non aveva supporto nativo per membri privati, esistono diverse tecniche per implementare l’incapsulamento efficacemente.
Cos’è l’Incapsulamento
L’incapsulamento serve a proteggere l’integrità dei dati e a fornire un controllo preciso su come gli oggetti vengono utilizzati. Nasconde la complessità interna e previene modifiche accidentali o non autorizzate dello stato interno dell’oggetto, esponendo solo i metodi e le proprietà necessarie per interagire con esso.
// Esempio senza incapsulamento - problematico
const utente = {
nome: "Mario",
email: "mario@email.com",
saldo: 1000,
};
// Chiunque può modificare direttamente il saldo!
utente.saldo = -500; // Potenzialmente pericoloso
Con l’incapsulamento, controlliamo l’accesso e la modifica dei dati attraverso metodi specifici che possono includere validazione e logica di business.
Incapsulamento con Closure
Le closure sono il metodo tradizionale per creare privacy in JavaScript, sfruttando il fatto che le variabili in uno scope esterno rimangono accessibili alle funzioni interne:
function creaContoCorrente(nomeIniziale, saldoIniziale) {
// Variabili private - accessibili solo tramite closure
let nome = nomeIniziale;
let saldo = saldoIniziale;
let transazioni = [];
// Funzione privata
function registraTransazione(tipo, importo) {
transazioni.push({
tipo,
importo,
data: new Date(),
saldoRimanente: saldo,
});
}
// Interfaccia pubblica
return {
getNome() {
return nome;
},
getSaldo() {
return saldo;
},
deposita(importo) {
if (importo <= 0) {
throw new Error("L'importo deve essere positivo");
}
saldo += importo;
registraTransazione("deposito", importo);
return saldo;
},
preleva(importo) {
if (importo <= 0) {
throw new Error("L'importo deve essere positivo");
}
if (importo > saldo) {
throw new Error("Saldo insufficiente");
}
saldo -= importo;
registraTransazione("prelievo", -importo);
return saldo;
},
getStoricoTransazioni() {
// Restituisce una copia per prevenire modifiche esterne
return [...transazioni];
},
};
}
// Utilizzo
const mioConto = creaContoCorrente("Mario Rossi", 1000);
console.log(mioConto.getSaldo()); // 1000
mioConto.deposita(500);
console.log(mioConto.getSaldo()); // 1500
// Impossibile accedere direttamente alle variabili private
// console.log(mioConto.saldo); // undefined
Incapsulamento con Classi e Convenzioni
Prima dei campi privati nativi, JavaScript utilizzava convenzioni di naming per indicare membri privati:
class ContoBancario {
constructor(nome, saldoIniziale) {
this.nome = nome;
this._saldo = saldoIniziale; // Convenzione: _ indica "privato"
this._transazioni = [];
}
// Metodo "privato" per convenzione
_registraTransazione(tipo, importo) {
this._transazioni.push({
tipo,
importo,
data: new Date(),
saldoRimanente: this._saldo,
});
}
// Metodi pubblici
getSaldo() {
return this._saldo;
}
deposita(importo) {
if (importo <= 0) {
throw new Error("L'importo deve essere positivo");
}
this._saldo += importo;
this._registraTransazione("deposito", importo);
return this._saldo;
}
preleva(importo) {
if (importo <= 0) {
throw new Error("L'importo deve essere positivo");
}
if (importo > this._saldo) {
throw new Error("Saldo insufficiente");
}
this._saldo -= importo;
this._registraTransazione("prelievo", -importo);
return this._saldo;
}
}
Questo approccio si basa sulla disciplina del team di sviluppo, poiché i membri “privati” sono comunque tecnicamente accessibili.
Campi Privati Nativi (ES2022)
JavaScript moderno supporta campi privati veri usando la sintassi #:
class ContoBancarioModerno {
// Campi privati - veri e propri, non accessibili dall'esterno
#saldo;
#transazioni;
#numeroContoInterno;
constructor(nome, saldoIniziale) {
this.nome = nome; // Pubblico
this.#saldo = saldoIniziale; // Privato
this.#transazioni = [];
this.#numeroContoInterno = this.#generaNumeroConto();
}
// Metodo privato
#generaNumeroConto() {
return Math.random().toString(36).substr(2, 9).toUpperCase();
}
#registraTransazione(tipo, importo) {
this.#transazioni.push({
tipo,
importo,
data: new Date(),
saldoRimanente: this.#saldo,
});
}
// Getter per accesso controllato
get saldo() {
return this.#saldo;
}
get numeroContoMascherato() {
const numero = this.#numeroContoInterno;
return `***${numero.slice(-3)}`;
}
// Metodi pubblici
deposita(importo) {
if (importo <= 0) {
throw new Error("L'importo deve essere positivo");
}
this.#saldo += importo;
this.#registraTransazione("deposito", importo);
return this.#saldo;
}
preleva(importo) {
if (importo <= 0) {
throw new Error("L'importo deve essere positivo");
}
if (importo > this.#saldo) {
throw new Error("Saldo insufficiente");
}
this.#saldo -= importo;
this.#registraTransazione("prelievo", -importo);
return this.#saldo;
}
getStoricoTransazioni() {
return [...this.#transazioni];
}
}
// Utilizzo
const conto = new ContoBancarioModerno("Mario", 1000);
console.log(conto.saldo); // 1000 (attraverso getter)
console.log(conto.numeroContoMascherato); // ***XYZ
// Questi generano errori
// console.log(conto.#saldo); // SyntaxError
// conto.#registraTransazione("test", 100); // SyntaxError
Incapsulamento con Simboli
I simboli offrono un altro modo per creare proprietà “semi-private”:
const _saldo = Symbol("saldo");
const _transazioni = Symbol("transazioni");
const _registraTransazione = Symbol("registraTransazione");
class ContoConSimboli {
constructor(nome, saldoIniziale) {
this.nome = nome;
this[_saldo] = saldoIniziale;
this[_transazioni] = [];
}
[_registraTransazione](tipo, importo) {
this[_transazioni].push({
tipo,
importo,
data: new Date(),
saldoRimanente: this[_saldo],
});
}
getSaldo() {
return this[_saldo];
}
deposita(importo) {
if (importo <= 0) {
throw new Error("L'importo deve essere positivo");
}
this[_saldo] += importo;
this[_registraTransazione]("deposito", importo);
return this[_saldo];
}
}
// I simboli non appaiono in Object.keys() o for...in
const conto = new ContoConSimboli("Mario", 1000);
console.log(Object.keys(conto)); // ["nome"]
WeakMap per Privacy
Le WeakMap possono essere usate per memorizzare dati privati associati alle istanze:
const datiPrivati = new WeakMap();
class ContoConWeakMap {
constructor(nome, saldoIniziale) {
this.nome = nome;
// Memorizza dati privati nella WeakMap
datiPrivati.set(this, {
saldo: saldoIniziale,
transazioni: [],
numeroContoInterno: this._generaNumeroConto(),
});
}
_generaNumeroConto() {
return Math.random().toString(36).substr(2, 9).toUpperCase();
}
getSaldo() {
return datiPrivati.get(this).saldo;
}
deposita(importo) {
if (importo <= 0) {
throw new Error("L'importo deve essere positivo");
}
const privati = datiPrivati.get(this);
privati.saldo += importo;
privati.transazioni.push({
tipo: "deposito",
importo,
data: new Date(),
saldoRimanente: privati.saldo,
});
return privati.saldo;
}
}
Pattern di Incapsulamento Avanzati
Factory Pattern con Incapsulamento
function creaConfigurationManager(configIniziale) {
// Stato privato
const config = { ...configIniziale };
const storiaModifiche = [];
// Validazioni private
function validaChiave(chiave) {
if (typeof chiave !== "string" || chiave.length === 0) {
throw new Error("Chiave non valida");
}
}
function registraModifica(chiave, vecchioValore, nuovoValore) {
storiaModifiche.push({
chiave,
vecchioValore,
nuovoValore,
timestamp: Date.now(),
});
}
// API pubblica
return Object.freeze({
get(chiave) {
validaChiave(chiave);
return config[chiave];
},
set(chiave, valore) {
validaChiave(chiave);
const vecchioValore = config[chiave];
config[chiave] = valore;
registraModifica(chiave, vecchioValore, valore);
},
getStoria() {
return [...storiaModifiche];
},
export() {
return { ...config };
},
});
}
Mixin con Incapsulamento
function creaControlloreCache() {
const cache = new Map();
const statistiche = {
hit: 0,
miss: 0,
set: 0,
};
return {
// Metodi che possono essere aggiunti ad altre classi
cache: {
get(chiave) {
if (cache.has(chiave)) {
statistiche.hit++;
return cache.get(chiave);
}
statistiche.miss++;
return undefined;
},
set(chiave, valore) {
cache.set(chiave, valore);
statistiche.set++;
},
getStatistiche() {
return { ...statistiche };
},
},
};
}
Best Practices
Usa campi privati nativi quando possibile: Offrono vera privacy e sono supportati nei browser moderni.
Implementa getter/setter per controllo: Invece di esporre proprietà direttamente, usa metodi che possono includere validazione.
Restituisci copie, non riferimenti: Per array e oggetti, restituisci copie per prevenire modifiche esterne accidentali.
Documenta l’interfaccia pubblica: Chiarisci cosa è pubblico e cosa è privato nella documentazione.
Valida gli input: I metodi pubblici dovrebbero sempre validare i parametri ricevuti.
L’incapsulamento in JavaScript è evoluto significativamente e ora offre strumenti potenti per creare codice robusto e manutenibile. La scelta della tecnica dipende dai requisiti del progetto, dal supporto browser necessario e dalle preferenze del team di sviluppo.
