INCAPSULAMENTO JAVASCRIPT

Uno dei concetti più importanti della programmazione ad oggetti è l’incapsulamento, ovvero la delimitazione delle interfacce interne da JS quelle esterne. Questa pratica è un “must” nello sviluppo di una qualsiasi applicazione che sia più complessa di “hello world”. Per comprenderla, usciamo dal mondo dello sviluppo e guardiamo al mondo reale. Solitamente, i dispositivi che utilizziamo sono piuttosto complessi. Poter delimitare la loro interfaccia interna da quella esterna, ci consente di utilizzarli senza grossi problemi.

UN ESEMPIO DAL MONDO REALE

Prendiamo l’esempio di una macchina del caffè. Semplice all’esterno: un bottone, un display, un paio di fori… E, ovviamente, il risultato: un ottimo caffè! :). Ci sono molti dettagli. Ma riusciamo comunque ad utilizzarla anche senza conoscerli. Le macchine del caffè sono piuttosto affidabili, giusto? Possono durare per anni, e solamente nel caso in cui qualcosa smetta di funzionare, le portiamo a riparare. Il segreto dietro all’affidabilità e alla semplicità di una macchina del caffè è che tutti i dettagli sono ottimizzati e nascosti. Se rimuovessimo la copertura della macchina del caffè, allora il suo utilizzo sarebbe molto più complesso (dove dovremmo premere?), e pericoloso (potremmo prendere la scossa). Come vedremo in seguito, nella programmazione gli oggetti sono come le macchine del caffè. Ma per poter nascondere i loro dettagli interni, non utilizzeremo una copertura di sicurezza, ma piuttosto una speciale sintassi del linguaggio ed alcune convenzioni.

INTERFACCIA INTERNA ED ESTERNA

Nella programmazione orientata agli oggetti, le proprietà ed i metodi sono divisi in due gruppi:

  • Interfaccia interna – metodi e proprietà, accessibili dagli altri metodi della classe, ma non dall’esterno.
  • Interfaccia esterna – metodi e proprietà, accessibili anche dall’esterno della classe.

Continuando con l’analogia della macchina del caffè, ciò che è nascosto internamente (una pompa, un meccanismo di riscaldamento e così via) è la sua interfaccia interna. L’interfaccia interna viene utilizzata per far funzionare l’oggetto, i suoi elementi interagiscono gli uni con gli altri. Ad esempio, la pompa è collegata al meccanismo di riscaldamento. Ma vista dall’esterno, la macchina del caffè è protetta da una copertura, in modo che nessuno possa accedervi. I dettagli sono nascosti ed inaccessibili, ma possiamo sfruttarne le caratteristiche tramite la sua interfaccia esterna. Quindi, tutto ciò di cui abbiamo bisogno per utilizzare un oggetto è la sua interfaccia esterna. Potremmo essere completamente inconsapevoli del suo funzionamento interno, e ciò andrebbe bene.

Questa era un’introduzione generale.

In JavaScript, esistono due tipi di campi per un oggetto (proprietà e metodi):

  • Pubblici: accessibili ovunque. Questi ne definiscono l’interfaccia esterna. Finora abbiamo sempre utilizzato proprietà e metodi pubblici.
  • Privati: accessibili solamente dall’interno della classe. Questi ne definiscono l’interfaccia interna.

In molti altri linguaggi di programmazione esiste anche il concetto di campo “protected” (protetto): accessibile solamente dall’interno della classe e da quelle che la estendono (come i campi privati, ma in aggiunta sono accessibili anche dalle classi che ereditano). Questi sono altrettanto utili per la definizione dell’interfaccia interna. Generalmente sono più diffusi dei campi privati, poiché solitamente la nostra intenzione è quella di renderli accessibili anche nelle sottoclassi. I campi protetti non sono implementati in JavaScript a livello di linguaggio, ma nella pratica risultano essere molto utili, per questo vengono spesso emulati. Ora costruiremo una macchina del caffè in JavaScript, con tutti i tipi di proprietà descritti. Una macchina del caffè è composta da molti dettagli; non la modelleremo per intero (anche se potremmo), in modo da mantenere l’esempio semplice.

CAMPI PROTETTI

Proviamo a modificare la proprietà waterAmount rendendola protetta, in modo da avere un maggior controllo su di essa. Ad esempio, non vorremmo che qualcuno possa impostarla con un valore negativo. Le proprietà protette, solitamente, vengono prefissate con un underscore _. Questa non è una forzatura del linguaggio, ma piuttosto una convenzione diffusa tra i programmatori, che specifica che queste proprietà e metodi non dovrebbero essere accessibili dall’esterno. Quindi la nostra proprietà diventa _waterAmount:

class CoffeeMachine {

  _waterAmount 0;

  set waterAmount(value) {

    if (value < 0) {

      value = 0;

    }

    this._waterAmount value;

  }

  get waterAmount() {

    return this._waterAmount;

  }

  constructor(power) {

    this._power power;

  }

}

// creiamo la macchina del caffè

let coffeeMachine new CoffeeMachine(100);

// aggiungiamo acqua

coffeeMachine.waterAmount = -10// _waterAmount diventerà 0, non -10

Ora l’accesso è sotto controllo, quindi non è più possibile impostare la quantità d’acqua ad un valore negativo.

PROPRIETA’ READ-ONLY

Proviamo a rendere la proprietà power come read-only (sola lettura). In alcuni casi, potremmo aver bisogno di definire una proprietà in fase di costruzione, e non volerla più modificare in seguito. Questo è esattamente il caso per una macchina del caffè: la potenza non può variare. Per farlo, possiamo semplicemente definire un getter, e nessun setter:

class CoffeeMachine {

  // …

  constructor(power) {

    this._power power;

  }

  get power() {

    return this._power;

  }

}

// creiamo la macchina del caffè

let coffeeMachine = new CoffeeMachine(100);

alert(`Power is: ${coffeeMachine.power}W`); // Power is: 100W

coffeeMachine.power = 25; // Errore (nessun setter)

I CAMPI PROTETTI VENGONO EREDITATI

Se ereditiamo class MegaMachine extends CoffeeMachine, allora nulla ci vieterà di accedere a this._waterAmount o this._power dai metodi nella nuova classe. Quindi, i metodi protetti vengono ereditati. A differenza di quelli privati, che vedremo tra poco.

CAMPI PRIVATI

I campi privati dovrebbero essere preceduti da #. Questi saranno accessibili solamente dall’interno della classe. Ad esempio, qui abbiamo una proprietà privata #waterLimit e un metodo privato per il controllo del livello dell’acqua #fixWaterAmount:

class CoffeeMachine {

  #waterLimit = 200;

  #fixWaterAmount(value) {

    if (value < 0) return 0;

    if (value this.#waterLimitreturn this.#waterLimit;

  }

  setWaterAmount(value) {

    this.#waterLimit = this.#fixWaterAmount(value);

  }

}

let coffeeMachine new CoffeeMachine();

// non possiamo accedere ai metodi privati dall’esterno della classe

coffeeMachine.#fixWaterAmount(123); // Errore

coffeeMachine.#waterLimit = 1000; // Errore

A livello di linguaggio, # è un carattere speciale per indicare che quel campo è privato. Non possiamo quindi accedervi dall’esterno o da una sotto-classe. Inoltre, i campi privati non entrano in conflitto con quelli pubblici. Possiamo avere sia un campo privato #waterAmount che uno pubblico waterAmount. Ad esempio, facciamo sì che waterAmount sia una proprietà per accedere a #waterAmount:

class CoffeeMachine {

  #waterAmount = 0;

  get waterAmount() {

    return this.#waterAmount;

  }

  set waterAmount(value) {

    if (value < 0value = 0;

    this.#waterAmount = value;

  }

}

let machine new CoffeeMachine();

machine.waterAmount 100;

alert(machine.#waterAmount); // Errore

A differenza di quelli protetti, i campi privati sono forniti dal linguaggio stesso. E questa è una buona cosa. Nel caso in cui stessimo ereditando da CoffeeMachine, allora non avremmo accesso diretto a #waterAmount. Dovremmo affidarci al getter/setter waterAmount. In molti casi, una limitazione del genere è troppo severa. Se estendiamo una CoffeeMachine, potremmo giustamente voler accedere ai suoi campi interni. Questo è il motivo per cui i campi protetti vengono usati più spesso, anche se non sono realmente supportati dalla sintassi del linguaggio.

Copy to Clipboard

APPROFONDIMENTO AI

Incapsulamento in JavaScript è un concetto fondamentale nella programmazione orientata agli oggetti (OOP), che consiste nel limitare l’accesso diretto a certe proprietà o metodi di un oggetto, in modo che possano essere modificati solo attraverso un’interfaccia controllata. Lo scopo principale è garantire che i dati interni di un oggetto siano protetti da modifiche accidentali o non intenzionali, migliorando così la sicurezza e la manutenibilità del codice.

Come implementare l’incapsulamento in JavaScript

1. Variabili e funzioni private: Prima di ECMAScript 2015 (ES6), JavaScript non aveva un modo nativo per creare variabili private. Tuttavia, è possibile ottenere l’incapsulamento utilizzando le closure e le funzioni costruttore.

Esempio con le closure:

function Persona(nome) {
     let _nome = nome; // Variabile privata

     this.getNome = function() {
        return _nome;
     };

     this.setNome = function(nuovoNome) {
       _nome = nuovoNome;
     };
}

const persona = new Persona(“Mario“);
console.log(persona.getNome()); // Mario
persona.setNome(“Luigi“);
console.log(persona.getNome()); // Luigi

In questo esempio, la variabile _nome non è direttamente accessibile dall’esterno, ma può essere letta o modificata tramite i metodi pubblici getNome e setNome. Questo tipo di incapsulamento si ottiene grazie al fatto che le funzioni interne di Persona hanno accesso alla variabile _nome, ma questa non è esposta direttamente.

2. Classi con ECMAScript 6 (ES6): Con l’introduzione delle classi in ES6, possiamo simulare l’incapsulamento utilizzando variabili debolmente private (usando una convenzione come il prefisso _ per indicare che una proprietà è privata).

Esempio con classi (simulazione):

class Persona {
       constructor(nome) {
          this._nome = nome; // Convenzione per variabili “private”
       }

       getNome() {
          return this._nome;
       }

       setNome(nuovoNome) {
          this._nome = nuovoNome;
       }
}

const persona = new Persona(“Mario“);
console.log(persona.getNome()); // Mario
persona.setNome(“Luigi“);
console.log(persona.getNome()); // Luigi

In questo caso, la variabile _nome viene trattata come “privata” per convenzione, anche se tecnicamente è accessibile dall’esterno.

3. Proprietà private con ECMAScript 2019 (ES10): A partire da ECMAScript 2019, JavaScript ha introdotto il supporto nativo per le proprietà private utilizzando il prefisso #.

Esempio con proprietà private:

class Persona {
       #nome; // Variabile privata

       constructor(nome) {
          this.#nome = nome;
       }

       getNome() {
          return this.#nome;
      }

      setNome(nuovoNome) {
          this.#nome = nuovoNome;
      }
}

const persona = new Persona(“Mario“);
console.log(persona.getNome()); // Mario
persona.setNome(“Luigi“);
console.log(persona.getNome()); // Luigi

// console.log(persona.#nome); // Errore: proprietà privata non accessibile

In questo esempio, la proprietà #nome è completamente privata e non può essere acceduta o modificata dall’esterno se non tramite i metodi pubblici definiti.

Vantaggi dell’incapsulamento

1. Protezione dei dati: Riduce il rischio di modifiche involontarie o non desiderate ai dati interni dell’oggetto.

2. Modularità: Ogni oggetto gestisce autonomamente i propri dati e comportamenti, facilitando la comprensione e la manutenzione del codice.

3. Flessibilità: È possibile cambiare l’implementazione interna di un oggetto senza modificare il codice esterno che lo utilizza, fintanto che l’interfaccia pubblica rimane la stessa.

Esempi di uso pratico

L’incapsulamento viene spesso utilizzato per:

•Limitare l’accesso ai dati sensibili.

•Impedire l’accesso diretto a dettagli implementativi che non dovrebbero essere esposti.

•Creare un’interfaccia stabile per gli oggetti, che può essere utilizzata dagli sviluppatori senza conoscere i dettagli interni.

Conclusione

L’incapsulamento in JavaScript è un concetto fondamentale per creare codice robusto e modulare. Con le tecniche di closure, le classi introdotte in ES6 e le proprietà private native a partire da ES10, è possibile implementare incapsulamento in vari modi, a seconda delle esigenze del progetto e della compatibilità con le versioni di JavaScript.

IL LINGUAGGIO JAVASCRIPT

IL LINGUAGGIO JAVASCRIPT

LINK AL CODICE SU GITHUB

GITHUB