Salta al contenuto principale

02 - Incapsulamento in OOP: Proteggere e Nascondere i Dati

Cos'è l'Incapsulamento nella Programmazione Orientata agli Oggetti L' incapsulamento è uno dei pilastri fondamentali della programmazione orientata agli...

Cosa ne pensi di questo articolo?

In Breve

Cos'è l'Incapsulamento nella Programmazione Orientata agli Oggetti L' incapsulamento è uno dei pilastri fondamentali della programmazione orientata agli...

Cos'è l'Incapsulamento nella Programmazione Orientata agli Oggetti

L'incapsulamento è uno dei pilastri fondamentali della programmazione orientata agli oggetti (OOP) e rappresenta il principio di nascondere i dettagli implementativi di una classe, esponendo solo ciò che è necessario attraverso un'interfaccia pubblica ben definita. Questo concetto è cruciale per creare software manutenibile, sicuro e modulare.

🎯 Obiettivi dell'Incapsulamento

  • Information Hiding: Nascondere i dettagli implementativi
  • Protezione Dati: Prevenire accessi e modifiche non autorizzate
  • Flessibilità: Cambiare implementazione senza impattare il codice esterno
  • Manutenibilità: Ridurre l'accoppiamento tra componenti

I Modificatori di Accesso in TypeScript

TypeScript offre tre modificatori di accesso per implementare l'incapsulamento: private, protected e public. La scelta del modificatore giusto determina chi può accedere ai membri della classe.

1. Private: Accesso Solo Interno

I membri private sono accessibili solo all'interno della classe che li definisce. Questo è il livello più restrittivo di incapsulamento e dovrebbe essere il default per tutti i dati interni della classe.

TypeScript - Membri Private
class BankAccount {{ '{' }}
  // Proprietà private - accesso solo interno
  private balance: number;
  private accountNumber: string;
  private transactionHistory: Transaction[] = [];
  private isLocked: boolean = false;

  constructor(accountNumber: string, initialBalance: number) {{ '{' }}
    this.accountNumber = accountNumber;
    this.balance = initialBalance;
    this.logTransaction('Account created', initialBalance);
  {{ '}' }}

  // Metodo pubblico con validazione
  public deposit(amount: number): void {{ '{' }}
    if (this.isLocked) {{ '{' }}
      throw new Error('Account is locked');
    {{ '}' }}

    if (amount <= 0) {{ '{' }}
      throw new Error('Amount must be positive');
    {{ '}' }}

    this.balance += amount;
    this.logTransaction('Deposit', amount);
  {{ '}' }}

  public withdraw(amount: number): boolean {{ '{' }}
    if (this.isLocked) {{ '{' }}
      throw new Error('Account is locked');
    {{ '}' }}

    if (amount > this.balance) {{ '{' }}
      console.log('Insufficient funds');
      return false;
    {{ '}' }}

    if (amount <= 0) {{ '{' }}
      throw new Error('Amount must be positive');
    {{ '}' }}

    this.balance -= amount;
    this.logTransaction('Withdrawal', amount);
    return true;
  {{ '}' }}

  // Getter pubblico - read-only access
  public getBalance(): number {{ '{' }}
    return this.balance;
  {{ '}' }}

  public getAccountNumber(): string {{ '{' }}
    return this.accountNumber;
  {{ '}' }}

  // Metodo privato - helper interno
  private logTransaction(type: string, amount: number): void {{ '{' }}
    const transaction: Transaction = {{ '{' }}
      id: Date.now().toString(),
      type,
      amount,
      timestamp: new Date(),
      balanceAfter: this.balance
    {{ '}' }};
    this.transactionHistory.push(transaction);
  {{ '}' }}

  // Metodo per ottenere copia della storia (non l'array originale!)
  public getTransactionHistory(): readonly Transaction[] {{ '{' }}
    return Object.freeze([...this.transactionHistory]);
  {{ '}' }}

  // Metodi amministrativi
  public lockAccount(): void {{ '{' }}
    this.isLocked = true;
  {{ '}' }}

  public unlockAccount(adminCode: string): void {{ '{' }}
    if (this.verifyAdminCode(adminCode)) {{ '{' }}
      this.isLocked = false;
    {{ '}' }}
  {{ '}' }}

  private verifyAdminCode(code: string): boolean {{ '{' }}
    // Logica di verifica sicura
    return code === 'ADMIN_SECRET_CODE';
  {{ '}' }}
{{ '}' }}

interface Transaction {{ '{' }}
  id: string;
  type: string;
  amount: number;
  timestamp: Date;
  balanceAfter: number;
{{ '}' }}

// Utilizzo
const account = new BankAccount('IT60X0542811101000000123456', 1000);

account.deposit(500);    // ✅ OK
console.log(account.getBalance()); // ✅ OK - 1500

// account.balance = 999999;  // ❌ ERRORE: balance è private
// account.logTransaction();  // ❌ ERRORE: logTransaction è private

✨ Vantaggi dei Membri Private

  • Controllo Totale: Solo la classe può modificare lo stato interno
  • Validazione Centralizzata: Tutte le modifiche passano per metodi pubblici validati
  • Refactoring Sicuro: Puoi cambiare implementazione privata senza rompere codice esterno
  • Debugging Facile: Sai esattamente dove lo stato viene modificato

2. Protected: Accesso per Sottoclassi

I membri protected sono accessibili dalla classe e dalle sue sottoclassi. Utili quando vuoi permettere l'estensione ma mantenere l'incapsulamento verso l'esterno.

TypeScript - Membri Protected
abstract class Vehicle {{ '{' }}
  protected engineStatus: 'on' | 'off' = 'off';
  protected currentSpeed: number = 0;
  protected readonly maxSpeed: number;

  constructor(maxSpeed: number) {{ '{' }}
    this.maxSpeed = maxSpeed;
  {{ '}' }}

  // Metodo protected - usabile da sottoclassi
  protected startEngine(): void {{ '{' }}
    if (this.engineStatus === 'off') {{ '{' }}
      this.engineStatus = 'on';
      console.log('Engine started');
    {{ '}' }}
  {{ '}' }}

  protected stopEngine(): void {{ '{' }}
    if (this.currentSpeed === 0) {{ '{' }}
      this.engineStatus = 'off';
      console.log('Engine stopped');
    {{ '}' }} else {{ '{' }}
      console.log('Cannot stop engine while moving');
    {{ '}' }}
  {{ '}' }}

  // Metodo pubblico
  public getSpeed(): number {{ '{' }}
    return this.currentSpeed;
  {{ '}' }}

  // Metodo astratto - deve essere implementato
  abstract accelerate(amount: number): void;
{{ '}' }}

class Car extends Vehicle {{ '{' }}
  private gear: number = 1;

  constructor() {{ '{' }}
    super(200); // maxSpeed per auto
  {{ '}' }}

  accelerate(amount: number): void {{ '{' }}
    // Posso accedere a membri protected del genitore
    if (this.engineStatus === 'off') {{ '{' }}
      this.startEngine(); // ✅ OK - protected method
    {{ '}' }}

    if (this.currentSpeed + amount <= this.maxSpeed) {{ '{' }}
      this.currentSpeed += amount;
      this.adjustGear();
    {{ '}' }}
  {{ '}' }}

  brake(amount: number): void {{ '{' }}
    this.currentSpeed = Math.max(0, this.currentSpeed - amount);

    if (this.currentSpeed === 0) {{ '{' }}
      this.stopEngine(); // ✅ OK - protected method
    {{ '}' }}
  {{ '}' }}

  private adjustGear(): void {{ '{' }}
    // Logica privata di cambio marcia
    if (this.currentSpeed > 80) this.gear = 5;
    else if (this.currentSpeed > 60) this.gear = 4;
    else if (this.currentSpeed > 40) this.gear = 3;
    else if (this.currentSpeed > 20) this.gear = 2;
    else this.gear = 1;
  {{ '}' }}
{{ '}' }}

// Utilizzo
const car = new Car();
car.accelerate(50);
console.log(car.getSpeed()); // 50

// car.startEngine();  // ❌ ERRORE: startEngine è protected
// car.engineStatus;   // ❌ ERRORE: engineStatus è protected

3. Public: Accesso Libero

I membri public sono accessibili da qualsiasi parte del codice. Questi costituiscono l'interfaccia pubblica della classe e dovrebbero essere minimizzati e ben documentati.

⚠️ Principio del Minimo Privilegio

Inizia sempre con private e rilassa solo quando necessario. È più facile rendere qualcosa public in seguito che rendere privato qualcosa che era public (breaking change).

  • Private by default
  • Protected solo se necessario per sottoclassi
  • Public solo per l'interfaccia essenziale

Getter e Setter: Accesso Controllato

I getter e setter in TypeScript offrono un modo elegante per controllare l'accesso alle proprietà, permettendo validazione e logica custom.

TypeScript - Getter e Setter
class User {{ '{' }}
  private _email: string;
  private _age: number;
  private _passwordHash: string;

  constructor(email: string, age: number, password: string) {{ '{' }}
    this._email = email;
    this._age = age;
    this._passwordHash = this.hashPassword(password);
  {{ '}' }}

  // Getter - read-only access con trasformazione
  get email(): string {{ '{' }}
    return this._email.toLowerCase();
  {{ '}' }}

  // Setter con validazione
  set email(value: string) {{ '{' }}
    if (!this.isValidEmail(value)) {{ '{' }}
      throw new Error('Invalid email format');
    {{ '}' }}
    this._email = value;
  {{ '}' }}

  // Getter
  get age(): number {{ '{' }}
    return this._age;
  {{ '}' }}

  // Setter con validazione
  set age(value: number) {{ '{' }}
    if (value < 0 || value > 150) {{ '{' }}
      throw new Error('Invalid age');
    {{ '}' }}
    this._age = value;
  {{ '}' }}

  // Proprietà computed - solo getter
  get isAdult(): boolean {{ '{' }}
    return this._age >= 18;
  {{ '}' }}

  get ageGroup(): string {{ '{' }}
    if (this._age < 18) return 'minor';
    if (this._age < 65) return 'adult';
    return 'senior';
  {{ '}' }}

  // Metodo pubblico per cambiare password
  changePassword(oldPassword: string, newPassword: string): boolean {{ '{' }}
    if (!this.verifyPassword(oldPassword)) {{ '{' }}
      return false;
    {{ '}' }}

    if (newPassword.length < 8) {{ '{' }}
      throw new Error('Password must be at least 8 characters');
    {{ '}' }}

    this._passwordHash = this.hashPassword(newPassword);
    return true;
  {{ '}' }}

  // Metodi privati - helper
  private isValidEmail(email: string): boolean {{ '{' }}
    const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
    return emailRegex.test(email);
  {{ '}' }}

  private hashPassword(password: string): string {{ '{' }}
    // Simulazione hashing (usa bcrypt in produzione!)
    return `hash_${{ '{' }}password.split('').reverse().join(''){{ '}' }}`;
  {{ '}' }}

  private verifyPassword(password: string): boolean {{ '{' }}
    return this.hashPassword(password) === this._passwordHash;
  {{ '}' }}
{{ '}' }}

// Utilizzo
const user = new User('john@example.com', 25, 'password123');

console.log(user.email);     // "john@example.com"
console.log(user.isAdult);   // true
console.log(user.ageGroup);  // "adult"

user.age = 30;               // ✅ OK - passa validazione
// user.age = -5;            // ❌ Errore: Invalid age
// user.email = 'invalid';   // ❌ Errore: Invalid email format

user.changePassword('password123', 'newSecurePass123'); // ✅ OK

// user._passwordHash;       // ❌ ERRORE: _passwordHash è private

✅ Quando Usare Getter/Setter:

  • Serve validazione dei dati
  • Trasformazione del valore
  • Proprietà computed
  • Logging delle modifiche
  • Lazy initialization

❌ Quando NON Usarli:

  • Semplice accesso senza logica
  • Solo per "sembrare OOP"
  • Calcoli pesanti nel getter
  • Side effects nel getter

Readonly: Immutabilità

Il modificatore readonly garantisce che una proprietà possa essere assegnata solo durante l'inizializzazione (costruttore) e non possa più essere modificata.

TypeScript - Readonly
class Configuration {{ '{' }}
  public readonly appName: string;
  public readonly version: string;
  private readonly apiKey: string;
  public readonly createdAt: Date;

  constructor(appName: string, version: string, apiKey: string) {{ '{' }}
    this.appName = appName;
    this.version = version;
    this.apiKey = apiKey;
    this.createdAt = new Date(); // Solo qui può essere assegnato
  {{ '}' }}

  // Shorthand syntax nel costruttore
  constructor(
    public readonly appName: string,
    public readonly version: string,
    private readonly apiKey: string
  ) {{ '{' }}
    // TypeScript crea automaticamente le proprietà
  {{ '}' }}

  getApiKey(): string {{ '{' }}
    return this.apiKey;
  {{ '}' }}
{{ '}' }}

const config = new Configuration('MyApp', '1.0.0', 'secret_key');
console.log(config.appName);  // "MyApp"
// config.appName = 'NewName'; // ❌ ERRORE: readonly property

Encapsulation Best Practices

🎯 Linee Guida Professionali

1. Tell, Don't Ask

Invece di chiedere lo stato e poi decidere, dì all'oggetto cosa fare:

❌ Male (Ask):
if (account.getBalance() > amount) {{ '{' }}
  account.setBalance(
    account.getBalance() - amount
  );
{{ '}' }}
✅ Bene (Tell):
account.withdraw(amount);

2. Law of Demeter (LoD)

Non parlare con gli sconosciuti - un oggetto dovrebbe chiamare solo:

  • Metodi di se stesso
  • Parametri passati al metodo
  • Oggetti che crea
  • Componenti diretti
❌ Violazione LoD:
user.getAddress()
    .getCity()
    .getZipCode()
✅ Rispetta LoD:
user.getCityZipCode()

3. Immutabilità quando Possibile

Favorisci oggetti immutabili - più facili da ragionare e thread-safe:

class Point {{ '{' }}
  constructor(
    public readonly x: number,
    public readonly y: number
  ) {{ '{' }}{{ '}' }}

  // Invece di modificare, ritorna nuovo oggetto
  move(dx: number, dy: number): Point {{ '{' }}
    return new Point(this.x + dx, this.y + dy);
  {{ '}' }}
{{ '}' }}

Errori Comuni da Evitare

⚠️ Anti-Pattern dell'Incapsulamento

1. Getter/Setter Inutili (JavaBeans Anti-Pattern)
// ❌ Male - non aggiunge valore
class Product {{ '{' }}
  private name: string;

  getName(): string {{ '{' }} return this.name; {{ '}' }}
  setName(name: string) {{ '{' }} this.name = name; {{ '}' }}
{{ '}' }}

// ✅ Meglio - proprietà pubblica se non serve logica
class Product {{ '{' }}
  public name: string;
{{ '}' }}
2. Esporre Collezioni Mutabili
// ❌ Male - array può essere modificato esternamente
getItems(): Item[] {{ '{' }}
  return this.items;
{{ '}' }}

// ✅ Bene - ritorna copia immutabile
getItems(): readonly Item[] {{ '{' }}
  return Object.freeze([...this.items]);
{{ '}' }}
3. Troppi Getter/Setter

Se hai 20 getter e setter, probabilmente la classe fa troppe cose (viola Single Responsibility).

Conclusione

L'incapsulamento è fondamentale per creare codice OOP di qualità. Nascondendo i dettagli implementativi e esponendo solo interfacce ben definite, ottieni codice più sicuro, manutenibile e flessibile. Ricorda: private by default, e rilassa solo quando strettamente necessario.

🎯 Punti Chiave

  • Usa private per tutti i dettagli interni
  • Usa protected solo per estensione controllata
  • Mantieni l'interfaccia pubblica minima e stabile
  • Valida sempre i dati in input
  • Preferisci getter/setter quando serve logica
  • Usa readonly per dati che non devono cambiare
  • Applica "Tell, Don't Ask" e Law of Demeter

📚 Prossimo nella Serie OOP

Nel prossimo articolo esploreremo Ereditarietà e Polimorfismo, vedendo come le classi possono estendersi e come sfruttare il polimorfismo per codice flessibile ed estensibile.

Domande frequenti

Cos'è l'incapsulamento nella programmazione orientata agli oggetti?

L'incapsulamento è uno dei pilastri fondamentali della programmazione orientata agli oggetti (OOP) e rappresenta il principio di nascondere i dettagli implementativi di una classe, esponendo solo ciò che è necessario attraverso un'interfaccia pubblica ben definita.

Quali sono i modificatori di accesso disponibili in TypeScript per implementare l'incapsulamento?

I modificatori di accesso disponibili in TypeScript per implementare l'incapsulamento sono private, protected e public.

Che cosa significa rendere una proprietà readonly in TypeScript?

Rendendo una proprietà readonly in TypeScript si garantisce che essa possa essere assegnata solo durante l'inizializzazione (costruttore) e non possa più essere modificata.

Perché è importante utilizzare l'incapsulamento nella programmazione orientata agli oggetti?

L'incapsulamento è fondamentale per creare codice OOP di qualità, poiché nasconde i dettagli implementativi e esponendo solo interfacce ben definite, ottieni codice più sicuro, manutenibile e flessibile.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti