Incapsulamento JavaScript

Edoardo Midali
Edoardo Midali

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.