OOP Avanzata: Ereditarietà e Polimorfismo

Padroneggia i concetti avanzati della programmazione a oggetti: ereditarietà, polimorfismo, classi astratte e interfacce

🎯 Cosa Imparerai in Questa Lezione

Nella lezione precedente abbiamo visto classi, oggetti e incapsulamento. Ora esploriamo
gli altri due pilastri della OOP: ereditarietà e polimorfismo.
Questi concetti ti permetteranno di creare gerarchie di classi, riutilizzare codice e
scrivere programmi più flessibili e manutenibili.

💡 Perché Sono Importanti?

Windows Forms usa massicciamente l’ereditarietà! Quando crei un Form,
stai creando una classe che eredita da System.Windows.Forms.Form.
Tutti i controlli (Button, TextBox, Label) ereditano da Control.
Senza capire l’ereditarietà, non puoi padroneggiare Windows Forms! 🎨

🌳 Ereditarietà (Inheritance)

L’ereditarietà permette a una classe (classe derivata o figlia) di acquisire
proprietà e metodi di un’altra classe (classe base o padre). Questo promuove il riutilizzo
del codice ed evita duplicazioni.

💡 Concetto con Esempio Reale

Pensa agli animali: tutti gli animali hanno caratteristiche comuni (nome, età, muoversi).
Ma un Cane ha anche caratteristiche specifiche (abbaia), così come un
Gatto (miagola). Invece di riscrivere nome/età per ogni animale,
creiamo una classe base Animale e facciamo ereditare Cane e Gatto!

Ereditarietà Base – Sintassi
// CLASSE BASE (o Padre o Superclasse)
public class Animale 
{
    // Proprietà comuni a tutti gli animali
    public string Nome { get; set; }
    public int Età { get; set; }
    
    // Costruttore
    public Animale(string nome, int età) 
    {
        Nome = nome;
        Età = età;
    }
    
    // Metodo comune
    public void Mangia() 
    {
        Console.WriteLine($"{Nome} sta mangiando...");
    }
    
    public virtual void FaiVerso() 
    {
        Console.WriteLine($"{Nome} fa un verso generico");
    }
}

// CLASSE DERIVATA (o Figlia o Sottoclasse)
// Sintassi: class Figlia : Padre
public class Cane : Animale 
{
    // Proprietà specifica del cane
    public string Razza { get; set; }
    
    // Costruttore - chiama il costruttore base con 'base'
    public Cane(string nome, int età, string razza) : base(nome, età) 
    {
        Razza = razza;
    }
    
    // Metodo specifico del cane
    public void Abbaia() 
    {
        Console.WriteLine($"{Nome}: Bau Bau!");
    }
    
    // Override del metodo base (polimorfismo!)
    public override void FaiVerso() 
    {
        Console.WriteLine($"{Nome}: Bau Bau!");
    }
}

public class Gatto : Animale 
{
    public string Colore { get; set; }
    
    public Gatto(string nome, int età, string colore) : base(nome, età) 
    {
        Colore = colore;
    }
    
    public void Miagola() 
    {
        Console.WriteLine($"{Nome}: Miao Miao!");
    }
    
    public override void FaiVerso() 
    {
        Console.WriteLine($"{Nome}: Miao Miao!");
    }
}

Utilizzo Ereditarietà
class Program 
{
    static void Main() 
    {
        // Creo un cane
        Cane cane = new Cane("Fido", 3, "Labrador");
        
        // Proprietà ereditate da Animale
        Console.WriteLine($"Nome: {cane.Nome}");      // ✅ Ereditato
        Console.WriteLine($"Età: {cane.Età}");        // ✅ Ereditato
        
        // Proprietà specifica di Cane
        Console.WriteLine($"Razza: {cane.Razza}");    // ✅ Proprio di Cane
        
        // Metodi ereditati
        cane.Mangia();        // ✅ Ereditato da Animale
        
        // Metodi specifici
        cane.Abbaia();        // ✅ Proprio di Cane
        cane.FaiVerso();      // ✅ Override da Animale
        
        Console.WriteLine();
        
        // Creo un gatto
        Gatto gatto = new Gatto("Felix", 2, "Arancione");
        gatto.Mangia();       // ✅ Ereditato
        gatto.Miagola();      // ✅ Proprio di Gatto
        gatto.FaiVerso();     // ✅ Override
    }
}

📌 Regole dell’Ereditarietà in C#

  • Una classe può ereditare da UNA SOLA classe base (no ereditarietà multipla)
  • Usa : NomeClasseBase per ereditare
  • La classe derivata eredita tutto (eccetto costruttori e membri privati non accessibili)
  • Usa base per chiamare il costruttore o metodi della classe padre
  • Usa protected per membri visibili solo a classe e derivate
1
Modificatore protected

Protected rende i membri accessibili alla classe e alle sue derivate.

Uso di protected
public class Veicolo 
{
    protected string marca;  // Accessibile a Veicolo e classi derivate
    private string modello;  // Solo Veicolo
    
    public Veicolo(string marca, string modello) 
    {
        this.marca = marca;
        this.modello = modello;
    }
    
    protected void AvviaMotore() 
    {
        Console.WriteLine($"{marca} - Motore avviato");
    }
}

public class Auto : Veicolo 
{
    public Auto(string marca, string modello) : base(marca, modello) 
    {
    }
    
    public void Guida() 
    {
        AvviaMotore();  // ✅ OK - protected accessibile
        Console.WriteLine($"{marca} in movimento");  // ✅ OK
        
        // Console.WriteLine(modello);  // ❌ ERRORE - private non accessibile
    }
}

2
Keyword base

Usa base per accedere a membri della classe padre.

Uso di base
public class Dipendente 
{
    public string Nome { get; set; }
    public decimal StipendioBase { get; set; }
    
    public Dipendente(string nome, decimal stipendio) 
    {
        Nome = nome;
        StipendioBase = stipendio;
    }
    
    public virtual decimal CalcolaStipendio() 
    {
        return StipendioBase;
    }
}

public class Manager : Dipendente 
{
    public decimal Bonus { get; set; }
    
    // Costruttore: chiama base per inizializzare Nome e StipendioBase
    public Manager(string nome, decimal stipendio, decimal bonus) 
        : base(nome, stipendio)  // ← Chiama costruttore padre
    {
        Bonus = bonus;
    }
    
    public override decimal CalcolaStipendio() 
    {
        // Usa base per chiamare metodo padre
        return base.CalcolaStipendio() + Bonus;
    }
}

// Utilizzo
Manager m = new Manager("Anna", 3000, 500);
Console.WriteLine($"Stipendio: {m.CalcolaStipendio()}");  // 3500

🎭 Polimorfismo

Il polimorfismo permette a oggetti di classi diverse di rispondere
allo stesso metodo in modo diverso. In C# si ottiene con virtual e override.

💡 Polimorfismo in Pratica

Tutti gli animali possono “fare un verso”, ma ogni animale lo fa in modo diverso:
il cane abbaia, il gatto miagola, il cavallo nitrisce. Stesso metodo (FaiVerso()),
comportamenti diversi! Questo è polimorfismo.

Polimorfismo – virtual e override
// CLASSE BASE
public class Forma 
{
    public string Nome { get; set; }
    
    // virtual = può essere sovrascritto nelle classi derivate
    public virtual double CalcolaArea() 
    {
        return 0;
    }
    
    public virtual void Disegna() 
    {
        Console.WriteLine($"Disegno una forma generica: {Nome}");
    }
}

// CLASSE DERIVATA 1
public class Cerchio : Forma 
{
    public double Raggio { get; set; }
    
    public Cerchio(double raggio) 
    {
        Nome = "Cerchio";
        Raggio = raggio;
    }
    
    // override = sovrascrive il metodo della classe base
    public override double CalcolaArea() 
    {
        return Math.PI * Raggio * Raggio;
    }
    
    public override void Disegna() 
    {
        Console.WriteLine($"Disegno un cerchio con raggio {Raggio}");
    }
}

// CLASSE DERIVATA 2
public class Rettangolo : Forma 
{
    public double Base { get; set; }
    public double Altezza { get; set; }
    
    public Rettangolo(double b, double h) 
    {
        Nome = "Rettangolo";
        Base = b;
        Altezza = h;
    }
    
    public override double CalcolaArea() 
    {
        return Base * Altezza;
    }
    
    public override void Disegna() 
    {
        Console.WriteLine($"Disegno un rettangolo {Base}x{Altezza}");
    }
}

Polimorfismo in Azione
class Program 
{
    static void Main() 
    {
        // ===== POLIMORFISMO =====
        // Array di Forma, ma contiene oggetti di classi derivate!
        Forma[] forme = new Forma[3];
        forme[0] = new Cerchio(5);
        forme[1] = new Rettangolo(4, 6);
        forme[2] = new Cerchio(3);
        
        Console.WriteLine("=== CALCOLO AREE ===");
        
        // Stesso metodo, comportamenti diversi!
        foreach (Forma forma in forme) 
        {
            forma.Disegna();  // Chiama il metodo override specifico!
            Console.WriteLine($"Area: {forma.CalcolaArea():F2}");
            Console.WriteLine();
        }
        
        // Output:
        // Disegno un cerchio con raggio 5
        // Area: 78.54
        //
        // Disegno un rettangolo 4x6
        // Area: 24.00
        //
        // Disegno un cerchio con raggio 3
        // Area: 28.27
    }
}

🎯 Vantaggi del Polimorfismo

Flessibilità

Tratta oggetti diversi in modo uniforme

Estensibilità

Aggiungi nuove classi senza modificare il codice esistente

Manutenibilità

Modifiche localizzate nelle classi specifiche

Codice Pulito

Evita enormi switch/if per gestire tipi diversi

⚡ virtual vs override vs new
Keyword Dove si usa Significato
virtual Classe base Il metodo può essere sovrascritto nelle classi derivate
override Classe derivata Sovrascrive un metodo virtual della classe base
new Classe derivata Nasconde (non sovrascrive) un metodo della classe base

🔷 Classi Astratte

Una classe astratta è una classe che non può essere istanziata
direttamente. Serve come base per altre classi e può contenere metodi astratti
(senza implementazione) che le classi derivate devono implementare.

💡 Quando Usare Classi Astratte

Usa classi astratte quando vuoi definire un “contratto” con alcune parti già implementate.
Ad esempio: tutti i veicoli hanno un motore e si muovono, ma il modo di muoversi
è diverso (auto su strada, aereo in cielo). La classe Veicolo può essere astratta!

Classe Astratta – Sintassi
// CLASSE ASTRATTA
public abstract class Veicolo 
{
    // Proprietà normali
    public string Marca { get; set; }
    public string Modello { get; set; }
    
    // Costruttore (viene chiamato dalle classi derivate)
    public Veicolo(string marca, string modello) 
    {
        Marca = marca;
        Modello = modello;
    }
    
    // Metodo normale (con implementazione)
    public void MostraInfo() 
    {
        Console.WriteLine($"{Marca} {Modello}");
    }
    
    // Metodo ASTRATTO (senza implementazione)
    // Le classi derivate DEVONO implementarlo
    public abstract void Muovi();
    
    // Metodo virtual (opzionale sovrascriverlo)
    public virtual void Ferma() 
    {
        Console.WriteLine("Veicolo fermato");
    }
}

// CLASSE DERIVATA 1
public class Auto : Veicolo 
{
    public Auto(string marca, string modello) : base(marca, modello) 
    {
    }
    
    // OBBLIGATORIO implementare metodo astratto
    public override void Muovi() 
    {
        Console.WriteLine($"{Marca} {Modello} si muove su strada");
    }
}

// CLASSE DERIVATA 2
public class Aereo : Veicolo 
{
    public int Altitudine { get; set; }
    
    public Aereo(string marca, string modello) : base(marca, modello) 
    {
    }
    
    // OBBLIGATORIO implementare metodo astratto
    public override void Muovi() 
    {
        Console.WriteLine($"{Marca} {Modello} vola a {Altitudine}m");
    }
    
    // Override opzionale
    public override void Ferma() 
    {
        Console.WriteLine($"{Marca} {Modello} atterra");
    }
}

Utilizzo Classi Astratte
class Program 
{
    static void Main() 
    {
        // Veicolo v = new Veicolo("Tesla", "Model 3");  
        // ❌ ERRORE! Non puoi istanziare classi astratte
        
        // ✅ OK - Istanzio classi derivate
        Auto auto = new Auto("Tesla", "Model 3");
        Aereo aereo = new Aereo("Boeing", "747");
        aereo.Altitudine = 10000;
        
        // Polimorfismo con classi astratte
        Veicolo[] veicoli = new Veicolo[] { auto, aereo };
        
        foreach (Veicolo v in veicoli) 
        {
            v.MostraInfo();
            v.Muovi();      // Chiama implementazione specifica!
            v.Ferma();
            Console.WriteLine();
        }
        
        // Output:
        // Tesla Model 3
        // Tesla Model 3 si muove su strada
        // Veicolo fermato
        //
        // Boeing 747
        // Boeing 747 vola a 10000m
        // Boeing 747 atterra
    }
}

📌 Caratteristiche Classi Astratte

  • Dichiarate con keyword abstract
  • Non possono essere istanziate con new
  • Possono avere metodi astratti (senza corpo) e normali (con corpo)
  • I metodi astratti devono essere implementati nelle classi derivate
  • Possono avere costruttori (chiamati dalle classi derivate)
  • Possono avere campi, proprietà, metodi normali

🔌 Interfacce

Un’interfaccia è un contratto che definisce quali metodi e proprietà
una classe deve implementare, ma senza fornire l’implementazione.
Le interfacce permettono il polimorfismo senza ereditarietà.

💡 Interfacce vs Classi Astratte

Interfaccia

  • Solo firma metodi (no implementazione)
  • Una classe può implementare MOLTE interfacce
  • Definisce “cosa” fare
Classe Astratta

  • Può avere implementazioni
  • Eredità singola
  • Definisce “cosa” e parte del “come”

Interfacce – Sintassi
// INTERFACCIA
// Convenzione: nome inizia con I
public interface IVolante 
{
    // Solo firme - NO implementazione!
    void Vola();
    void Atterra();
    int AltitudineMax { get; set; }  // Proprietà
}

public interface INuotante 
{
    void Nuota();
    void Immergiti(int profondità);
}

// Classe che implementa UN'interfaccia
public class Aereo : IVolante 
{
    public int AltitudineMax { get; set; }
    
    // DEVE implementare tutti i membri dell'interfaccia
    public void Vola() 
    {
        Console.WriteLine("Aereo decolla e vola");
    }
    
    public void Atterra() 
    {
        Console.WriteLine("Aereo atterra");
    }
}

// Classe che implementa DUE interfacce!
public class Anatra : IVolante, INuotante 
{
    public int AltitudineMax { get; set; }
    
    // Implementa IVolante
    public void Vola() 
    {
        Console.WriteLine("Anatra vola basso");
    }
    
    public void Atterra() 
    {
        Console.WriteLine("Anatra si posa");
    }
    
    // Implementa INuotante
    public void Nuota() 
    {
        Console.WriteLine("Anatra nuota in superficie");
    }
    
    public void Immergiti(int profondità) 
    {
        Console.WriteLine($"Anatra si immerge a {profondità}cm");
    }
}

Utilizzo Interfacce
class Program 
{
    static void Main() 
    {
        Aereo aereo = new Aereo { AltitudineMax = 12000 };
        Anatra anatra = new Anatra { AltitudineMax = 100 };
        
        // ===== POLIMORFISMO CON INTERFACCE =====
        
        // Array di IVolante (interfaccia!)
        IVolante[] volanti = new IVolante[] { aereo, anatra };
        
        Console.WriteLine("=== COSE CHE VOLANO ===");
        foreach (IVolante v in volanti) 
        {
            v.Vola();
            Console.WriteLine($"Altitudine max: {v.AltitudineMax}m");
        }
        
        Console.WriteLine("\n=== COSE CHE NUOTANO ===");
        // Solo l'anatra implementa INuotante
        INuotante nuotatore = anatra;
        nuotatore.Nuota();
        nuotatore.Immergiti(50);
    }
}

🎯 Quando Usare Interfacce

  • Vuoi definire un contratto senza implementazione
  • Diverse classi non correlate devono avere gli stessi metodi
  • Serve implementare multiple interfacce (no ereditarietà multipla)
  • Esempio: IComparable, IEnumerable, IDisposable
💡 Interfacce Built-in Utili
Interfaccia Scopo Metodo Principale
IComparable Confrontare oggetti CompareTo()
IEnumerable Iterare con foreach GetEnumerator()
IDisposable Liberare risorse Dispose()
IEquatable<T> Confronto uguaglianza Equals()

🚀 Esempio Completo: Sistema Gestione Dipendenti

Vediamo un sistema completo che usa ereditarietà, polimorfismo, classi astratte e interfacce.

Interfacce.cs
using System;

// Interfaccia per entità che possono lavorare
public interface ILavoratore 
{
    void Lavora();
    decimal CalcolaStipendio();
}

// Interfaccia per entità che possono gestire altri
public interface IManager 
{
    void GestisciTeam();
    int NumeroSottoposti { get; set; }
}

Dipendente.cs – Classe Astratta Base
// Classe astratta base
public abstract class Dipendente : ILavoratore 
{
    public string Nome { get; set; }
    public string Cognome { get; set; }
    public string Matricola { get; set; }
    protected decimal stipendioBase;
    
    public Dipendente(string nome, string cognome, string matricola, decimal stipendio) 
    {
        Nome = nome;
        Cognome = cognome;
        Matricola = matricola;
        stipendioBase = stipendio;
    }
    
    // Metodo normale
    public string GetNomeCompleto() 
    {
        return $"{Nome} {Cognome}";
    }
    
    // Metodo astratto - ogni tipo di dipendente lavora diversamente
    public abstract void Lavora();
    
    // Metodo virtual - può essere sovrascritto
    public virtual decimal CalcolaStipendio() 
    {
        return stipendioBase;
    }
    
    public virtual void MostraInfo() 
    {
        Console.WriteLine($"📋 {GetNomeCompleto()} ({Matricola})");
        Console.WriteLine($"   Stipendio: €{CalcolaStipendio():N2}");
    }
}

Classi Derivate.cs
// DIPENDENTE OPERATIVO
public class DipendenteOperativo : Dipendente 
{
    public string Reparto { get; set; }
    
    public DipendenteOperativo(string nome, string cognome, string matricola, 
                               decimal stipendio, string reparto) 
        : base(nome, cognome, matricola, stipendio) 
    {
        Reparto = reparto;
    }
    
    public override void Lavora() 
    {
        Console.WriteLine($"{GetNomeCompleto()} lavora nel reparto {Reparto}");
    }
    
    public override void MostraInfo() 
    {
        base.MostraInfo();
        Console.WriteLine($"   Reparto: {Reparto}");
    }
}

// MANAGER (Dipendente + IManager)
public class ManagerReparto : Dipendente, IManager 
{
    public decimal Bonus { get; set; }
    public int NumeroSottoposti { get; set; }
    
    public ManagerReparto(string nome, string cognome, string matricola, 
                          decimal stipendio, decimal bonus) 
        : base(nome, cognome, matricola, stipendio) 
    {
        Bonus = bonus;
        NumeroSottoposti = 0;
    }
    
    public override void Lavora() 
    {
        Console.WriteLine($"{GetNomeCompleto()} coordina il team");
        GestisciTeam();
    }
    
    public void GestisciTeam() 
    {
        Console.WriteLine($"   Gestisce {NumeroSottoposti} dipendenti");
    }
    
    public override decimal CalcolaStipendio() 
    {
        return base.CalcolaStipendio() + Bonus;
    }
    
    public override void MostraInfo() 
    {
        base.MostraInfo();
        Console.WriteLine($"   Bonus: €{Bonus:N2}");
        Console.WriteLine($"   Team: {NumeroSottoposti} persone");
    }
}

// TECNICO (competenze specifiche)
public class Tecnico : Dipendente 
{
    public string Specializzazione { get; set; }
    public int AnniEsperienza { get; set; }
    
    public Tecnico(string nome, string cognome, string matricola, 
                   decimal stipendio, string specializzazione, int anni) 
        : base(nome, cognome, matricola, stipendio) 
    {
        Specializzazione = specializzazione;
        AnniEsperienza = anni;
    }
    
    public override void Lavora() 
    {
        Console.WriteLine($"{GetNomeCompleto()} lavora come {Specializzazione}");
    }
    
    public override decimal CalcolaStipendio() 
    {
        // Bonus per esperienza: €50 per anno
        decimal bonusEsperienza = AnniEsperienza * 50;
        return base.CalcolaStipendio() + bonusEsperienza;
    }
    
    public override void MostraInfo() 
    {
        base.MostraInfo();
        Console.WriteLine($"   Specializzazione: {Specializzazione}");
        Console.WriteLine($"   Esperienza: {AnniEsperienza} anni");
    }
}

Program.cs – Test Completo
using System;
using System.Collections.Generic;

class Program 
{
    static void Main() 
    {
        Console.WriteLine("=== SISTEMA GESTIONE DIPENDENTI ===\n");
        
        // ===== CREAZIONE DIPENDENTI =====
        List dipendenti = new List();
        
        DipendenteOperativo op1 = new DipendenteOperativo(
            "Mario", "Rossi", "D001", 1500, "Produzione"
        );
        
        DipendenteOperativo op2 = new DipendenteOperativo(
            "Luigi", "Verdi", "D002", 1500, "Logistica"
        );
        
        Tecnico tec1 = new Tecnico(
            "Anna", "Bianchi", "T001", 2000, "Elettronica", 5
        );
        
        ManagerReparto mgr1 = new ManagerReparto(
            "Paolo", "Neri", "M001", 3000, 500
        );
        mgr1.NumeroSottoposti = 3;
        
        dipendenti.Add(op1);
        dipendenti.Add(op2);
        dipendenti.Add(tec1);
        dipendenti.Add(mgr1);
        
        // ===== POLIMORFISMO IN AZIONE =====
        Console.WriteLine("--- ELENCO DIPENDENTI ---\n");
        
        foreach (Dipendente d in dipendenti) 
        {
            d.MostraInfo();  // Chiama versione specifica!
            d.Lavora();      // Metodo astratto implementato
            Console.WriteLine();
        }
        
        // ===== CALCOLO STIPENDI TOTALI =====
        decimal totaleStipendi = 0;
        
        foreach (Dipendente d in dipendenti) 
        {
            totaleStipendi += d.CalcolaStipendio();
        }
        
        Console.WriteLine($"💰 Totale stipendi: €{totaleStipendi:N2}");
        
        // ===== FILTRAGGIO PER INTERFACCIA =====
        Console.WriteLine("\n--- MANAGER ---");
        
        foreach (Dipendente d in dipendenti) 
        {
            // Verifica se implementa IManager
            if (d is IManager manager) 
            {
                Console.WriteLine($"{d.GetNomeCompleto()}:");
                manager.GestisciTeam();
            }
        }
        
        // ===== STATISTICHE =====
        Console.WriteLine("\n--- STATISTICHE ---");
        Console.WriteLine($"Totale dipendenti: {dipendenti.Count}");
        Console.WriteLine($"Stipendio medio: €{totaleStipendi / dipendenti.Count:N2}");
        
        // Trova stipendio massimo
        decimal maxStipendio = 0;
        string nomeMassimo = "";
        
        foreach (Dipendente d in dipendenti) 
        {
            decimal stipendio = d.CalcolaStipendio();
            if (stipendio > maxStipendio) 
            {
                maxStipendio = stipendio;
                nomeMassimo = d.GetNomeCompleto();
            }
        }
        
        Console.WriteLine($"Stipendio più alto: {nomeMassimo} (€{maxStipendio:N2})");
    }
}

💡 Analisi Architettura OOP Avanzata
  • Classe astratta Dipendente: Base comune con metodo astratto Lavora()
  • Ereditarietà: 3 classi derivate con implementazioni specifiche
  • Polimorfismo: Stesso array, oggetti diversi, comportamenti diversi
  • Interfacce: ILavoratore e IManager per contratti
  • Override: CalcolaStipendio() personalizzato per ogni tipo
  • Casting con is: Verifica se oggetto implementa interfaccia

🎯 Riepilogo Lezione

Ereditarietà:

  • Riutilizzo codice
  • Gerarchie di classi
  • Keyword: : Base, base
  • Modificatore protected
Polimorfismo:

  • virtual e override
  • Stesso metodo, comportamenti diversi
  • Flessibilità runtime
Classi Astratte:

  • Non istanziabili
  • Metodi astratti obbligatori
  • Contratto + implementazione parziale
Interfacce:

  • Solo contratto (no implementazione)
  • Implementazioni multiple
  • Polimorfismo senza ereditarietà

Lezione 14 del Corso C# Completo

Torna in alto