Information Hiding e Incapsulamento in C#

Information Hiding e Incapsulamento

  • Nascondere i dettagli interni di una classe
  • Esporre solo ciò che serve all'esterno
  • Proteggere lo stato dell'oggetto da usi scorretti

Motivazioni

  • L'incapsulamento, insieme all'ereditarietà e al polimorfismo, è uno dei tre pilastri della programmazione orientata agli oggetti.
  • Permette di trattare un oggetto come una scatola nera: si conosce ciò che fa, non come lo fa.
  • Riduce la complessità: chi usa la classe non deve preoccuparsi dei dettagli implementativi.
  • Si possono cambiare i dettagli interni senza rompere il codice che usa la classe.
  • Si evitano stati incoerenti perché i dati sono modificabili solo attraverso operazioni controllate.
  • Il codice diventa più facile da mantenere, leggere e riutilizzare.

Due concetti distinti ma legati

  • Information hiding: principio di progettazione → nascondere ciò che può cambiare
  • Incapsulamento: meccanismo del linguaggio → racchiudere dati e operazioni in una classe e regolarne la visibilità

L'incapsulamento è lo strumento con cui si realizza l'information hiding.

Il problema: dati esposti

class ContoCorrente
{
    public string Intestatario;
    public decimal Saldo;       // chiunque può modificarlo!
}

ContoCorrente c = new ContoCorrente();
c.Intestatario = "Mario Rossi";
c.Saldo        =  1000m;
c.Saldo        = -50000m;       // stato incoerente: nessun controllo
  • Nessuna validazione: il saldo può diventare negativo arbitrariamente
  • Nessun controllo su chi modifica i dati e come
  • Cambiare il tipo o il nome del campo rompe tutto il codice che lo usa

Modificatori di Accesso

I livelli di visibilità in C#

Modificatore Stessa classe Classe derivata Esterno
public
protected no
private no no

In C# il default per i membri di una classe è private.

La regola pratica

  • I campi (variabili di istanza) si dichiarano sempre private
  • L'accesso ai dati avviene tramite metodi o proprietà pubblici
  • Si espone cosa fa la classe, non come lo fa
  • Più ristretta è la visibilità, meglio è: si parte da private e si allarga solo se necessario

Esempio: campi privati e metodi pubblici

class ContoCorrente
{
    private string  _intestatario;   // nascosto
    private decimal _saldo;          // nascosto

    public ContoCorrente(string intestatario, decimal saldoIniziale)
    {
        _intestatario = intestatario;
        _saldo        = saldoIniziale >= 0 ? saldoIniziale : 0;
    }

    public void Versa(decimal importo)
    {
        if (importo > 0) _saldo += importo;
    }

    public bool Preleva(decimal importo)
    {
        if (importo <= 0 || importo > _saldo) return false;
        _saldo -= importo;
        return true;
    }

    public decimal LeggiSaldo() { return _saldo; }
}

Convenzione sui nomi

  • I campi privati iniziano con un underscore: _saldo, _intestatario
  • Le proprietà e i metodi pubblici usano il PascalCase: Saldo, Versa
  • Aiuta a distinguere a colpo d'occhio cosa è interno e cosa è esposto

Proprietà

Dai metodi getter/setter alle proprietà

In altri linguaggi (Java) si scrivono coppie di metodi:

public string GetIntestatario()       { return _intestatario; }
public void   SetIntestatario(string v) { _intestatario = v;  }

C# offre una sintassi dedicata: le proprietà.

public string Intestatario
{
    get { return _intestatario; }
    set { _intestatario = value; }
}

Si usano come campi (c.Intestatario = "...") ma sotto sono metodi.

Anatomia di una proprietà

class ContoCorrente
{
    private decimal _saldo;

    public decimal Saldo
    {
        get { return _saldo; }              // accessor di lettura
        private set { _saldo = value; }     // accessor di scrittura
    }
}
  • get: codice eseguito quando si legge la proprietà
  • set: codice eseguito quando si assegna alla proprietà; value contiene il nuovo valore
  • Ogni accessor può avere una visibilità propria (qui set è privato)

Proprietà con validazione

class Persona
{
    private int _eta;

    public int Eta
    {
        get { return _eta; }
        set
        {
            if (value < 0 || value > 130)
                throw new ArgumentOutOfRangeException(nameof(value));
            _eta = value;
        }
    }
}

Persona p = new Persona();
p.Eta = 25;     // OK
p.Eta = -3;     // eccezione: stato non valido

La proprietà fa da guardiano del campo.

Proprietà di sola lettura

class ContoCorrente
{
    private decimal _saldo;

    // Solo get: dall'esterno è leggibile ma non scrivibile
    public decimal Saldo { get { return _saldo; } }

    public void Versa(decimal importo)
    {
        if (importo > 0) _saldo += importo;
    }
}

ContoCorrente c = new ContoCorrente();
decimal x = c.Saldo;     // OK: lettura
// c.Saldo = 1000m;      // ERRORE: nessun setter pubblico

Proprietà automatiche

Quando il get/set non contiene logica, si può usare la sintassi sintetica:

class Persona
{
    // Il compilatore genera automaticamente un campo nascosto
    public string Nome    { get; set; }
    public int    Eta     { get; set; }

    // Sola lettura dall'esterno: si imposta solo dal costruttore
    public string Codice  { get; }

    public Persona(string nome, int eta, string codice)
    {
        Nome   = nome;
        Eta    = eta;
        Codice = codice;
    }
}

Set privato e proprietà calcolate

class Rettangolo
{
    public double Base    { get; private set; }
    public double Altezza { get; private set; }

    // Proprietà calcolata: nessun campo, valore derivato dagli altri
    public double Area => Base * Altezza;

    public Rettangolo(double b, double h)
    {
        Base    = b;
        Altezza = h;
    }

    public void Scala(double fattore)
    {
        Base    *= fattore;
        Altezza *= fattore;
    }
}

Information Hiding in pratica

Nascondere la rappresentazione

L'esterno vede una temperatura: come è memorizzata internamente non importa.

class Temperatura
{
    private double _kelvin;     // scelta interna: gradi Kelvin

    public double Celsius
    {
        get { return _kelvin - 273.15; }
        set { _kelvin = value + 273.15; }
    }

    public double Fahrenheit
    {
        get { return _kelvin * 9.0 / 5.0 - 459.67; }
        set { _kelvin = (value + 459.67) * 5.0 / 9.0; }
    }
}

Domani si può cambiare _kelvin in _celsius senza che il codice cliente se ne accorga.

Nascondere gli helper

Non tutti i metodi devono essere pubblici: quelli ausiliari restano private.

class CodiceFiscale
{
    private string _codice;

    public CodiceFiscale(string codice)
    {
        if (!Valida(codice))
            throw new ArgumentException("Codice fiscale non valido");
        _codice = codice.ToUpper();
    }

    public string Codice { get { return _codice; } }

    // Dettaglio implementativo: non interessa a chi usa la classe
    private static bool Valida(string c)
    {
        return c != null && c.Length == 16;
    }
}

Mantenere invarianti

Un invariante è una proprietà che deve essere sempre vera per gli oggetti di una classe.

class Frazione
{
    // Invariante: denominatore != 0, frazione sempre semplificata
    private int _num;
    private int _den;

    public int Numeratore   { get { return _num; } }
    public int Denominatore { get { return _den; } }

    public Frazione(int num, int den)
    {
        if (den == 0) throw new ArgumentException("Denominatore zero");
        int g = MCD(Math.Abs(num), Math.Abs(den));
        _num = num / g;
        _den = den / g;
    }

    private static int MCD(int a, int b) => b == 0 ? a : MCD(b, a % b);
}

L'incapsulamento garantisce che chi usa Frazione non possa mai romperne lo stato.

Esempio completo: Stack

Una struttura dati incapsulata

class Stack
{
    private int[] _dati;
    private int   _cima;       // indice del prossimo slot libero

    public Stack(int capacita)
    {
        _dati = new int[capacita];
        _cima = 0;
    }

    public int  Conteggio => _cima;
    public bool Vuoto     => _cima == 0;
    public bool Pieno     => _cima == _dati.Length;

    public void Push(int v)
    {
        if (Pieno) throw new InvalidOperationException("Stack pieno");
        _dati[_cima++] = v;
    }

    public int Pop()
    {
        if (Vuoto) throw new InvalidOperationException("Stack vuoto");
        return _dati[--_cima];
    }
}

Cosa è nascosto, cosa è esposto

  • Nascosto: l'array _dati, l'indice _cima, la capacità interna
  • Esposto: Push, Pop, Conteggio, Vuoto, Pieno
  • L'utente non può corrompere lo stato modificando _dati[_cima] = ...
  • Si potrebbe sostituire l'array con una List<int> senza modificare nulla all'esterno

Utilizzo

Stack s = new Stack(10);

s.Push(1);
s.Push(2);
s.Push(3);

Console.WriteLine(s.Conteggio);  // 3
Console.WriteLine(s.Pop());      // 3
Console.WriteLine(s.Pop());      // 2
Console.WriteLine(s.Vuoto);      // false

// s._dati[0] = 99;     // ERRORE: campo privato
// s._cima   = -1;      // ERRORE: campo privato

L'oggetto è una scatola nera: si interagisce solo con l'interfaccia pubblica.

Vantaggi dell'incapsulamento

Riassumendo

  • Stato consistente: i controlli nei setter e nei metodi mantengono gli invarianti
  • Modularità: ogni classe è responsabile dei propri dati
  • Manutenibilità: i dettagli interni si possono cambiare senza impatto sull'esterno
  • Riusabilità: una classe ben incapsulata si riusa con fiducia
  • Testabilità: l'interfaccia pubblica definisce esattamente cosa testare
  • Sicurezza: si limita la superficie esposta a usi scorretti o malevoli

Linee guida

  • I campi sono sempre private (o al massimo protected se serve a una gerarchia)
  • L'accesso passa da proprietà o metodi
  • Esporre il minimo indispensabile: ogni membro public è un impegno verso l'esterno
  • Validare gli input al confine della classe (costruttore, setter, metodi)
  • Preferire proprietà di sola lettura quando lo stato non deve cambiare dopo la creazione

Concetti chiave

Concetto Sintassi C#
Campo privato private tipo _campo;
Proprietà completa public T P { get { ... } set { ... } }
Proprietà automatica public T P { get; set; }
Sola lettura public T P { get; }
Set privato public T P { get; private set; }
Concetto Sintassi C#
Proprietà calcolata public T P => espressione;
Validazione nel setter controllo su value prima di assegnare
Metodo helper interno private invece di public
Convenzione campo privato nome con underscore: _saldo

Quando usare cosa

  • Campo privato + proprietà: caso generale, permette di aggiungere logica in futuro
  • Proprietà automatica: nessuna logica, solo lettura/scrittura semplice
  • Sola lettura: lo stato si fissa nel costruttore e non cambia più
  • Set privato: lo stato cambia solo tramite metodi della classe stessa
  • Proprietà calcolata: il valore deriva da altri campi e non va memorizzato

Fine

"Programs must be written for people to read, and only incidentally for machines to execute." — Harold Abelson