OOP: Concetti Avanzati in C#

Ereditarietà, polimorfismo, interfacce e classi astratte per codice riutilizzabile

🎯 Introduzione ai Concetti Avanzati OOP

Dopo aver compreso le basi della programmazione orientata agli oggetti, è tempo di esplorare
i concetti avanzati che rendono l’OOP uno strumento potente per creare
applicazioni complesse, manutenibili e scalabili. Questi concetti includono ereditarietà,
polimorfismo, interfacce e classi astratte.

💡 Perché Studiare i Concetti Avanzati?

I concetti avanzati dell’OOP permettono di riutilizzare il codice,
creare gerarchie di classi logiche e scrivere programmi più flessibili. Sono essenziali
per lavorare su progetti reali e comprendere framework come ASP.NET e Entity Framework.

👨‍👦 Ereditarietà

L’ereditarietà è un meccanismo che permette a una classe (derivata o figlia)
di ereditare caratteristiche e comportamenti da un’altra classe (base o padre). È uno dei
pilastri dell’OOP che favorisce il riutilizzo del codice e la creazione di gerarchie di classi.

Classe Base e Classe Derivata
// Classe base
public class Veicolo 
{
    public string Marca { get; set; }
    public string Modello { get; set; }
    public int Anno { get; set; }
    
    public Veicolo(string marca, string modello, int anno) 
    {
        Marca = marca;
        Modello = modello;
        Anno = anno;
    }
    
    public virtual void Descrivi() 
    {
        Console.WriteLine($"Veicolo: {Marca} {Modello}, Anno: {Anno}");
    }
    
    public virtual double CalcolaValoreAttuale() 
    {
        // Implementazione di base
        int etaVeicolo = DateTime.Now.Year - Anno;
        return 50000 - (etaVeicolo * 1000);
    }
}

// Classe derivata
public class Automobile : Veicolo 
{
    public int NumeroPorte { get; set; }
    public string Alimentazione { get; set; }
    
    public Automobile(string marca, string modello, int anno, 
                     int numeroPorte, string alimentazione) 
        : base(marca, modello, anno)  // Chiama il costruttore della classe base
    {
        NumeroPorte = numeroPorte;
        Alimentazione = alimentazione;
    }
    
    public override void Descrivi() 
    {
        base.Descrivi(); // Chiama il metodo della classe base
        Console.WriteLine($"Porte: {NumeroPorte}, Alimentazione: {Alimentazione}");
    }
    
    public override double CalcolaValoreAttuale() 
    {
        double valoreBase = base.CalcolaValoreAttuale();
        
        // Modifica in base all'alimentazione
        if (Alimentazione == "Elettrica") 
        {
            return valoreBase * 1.5; // Bonus per veicoli elettrici
        }
        
        return valoreBase;
    }
}

📌 Parole Chiave Importanti

virtual

  • Metodo che può essere sovrascritto
  • Nella classe base
  • Fornisce implementazione di default
override

  • Sovrascrive metodo virtuale
  • Nella classe derivata
  • Può chiamare base.Metodo()

🔗 Relazione “È UN”

L’ereditarietà dovrebbe essere utilizzata quando esiste una relazione “è un”
tra le classi. Un’automobile “è un” veicolo, quindi ha senso che
Automobile erediti da Veicolo. In C#, una classe può ereditare da una sola classe
(ereditarietà singola), ma può implementare più interfacce.

Esempio di Utilizzo
// Creazione oggetti
Automobile auto = new Automobile("Tesla", "Model 3", 2021, 4, "Elettrica");

auto.Descrivi();
// Output:
// Veicolo: Tesla Model 3, Anno: 2021
// Porte: 4, Alimentazione: Elettrica

double valore = auto.CalcolaValoreAttuale();
Console.WriteLine($"Valore stimato: {valore:C}");

🎭 Polimorfismo

Il polimorfismo (dal greco “molte forme”) permette a oggetti di classi diverse
di essere trattati come oggetti di una classe comune. In C# esistono due tipi principali di
polimorfismo.

Polimorfismo in Tempo di Compilazione (Overloading)

Avviene quando più metodi nella stessa classe hanno lo stesso nome ma parametri diversi.

Method Overloading
public class Calcolatrice 
{
    // Overloading dei metodi - stesso nome, parametri diversi
    public int Somma(int a, int b) 
    {
        return a + b;
    }
    
    public double Somma(double a, double b) 
    {
        return a + b;
    }
    
    public int Somma(int a, int b, int c) 
    {
        return a + b + c;
    }
    
    public string Somma(string a, string b) 
    {
        return a + b; // Concatenazione stringhe
    }
}

// Utilizzo
Calcolatrice calc = new Calcolatrice();
Console.WriteLine(calc.Somma(5, 3));           // 8 (int)
Console.WriteLine(calc.Somma(5.5, 3.2));       // 8.7 (double)
Console.WriteLine(calc.Somma(5, 3, 2));        // 10 (int)
Console.WriteLine(calc.Somma("Ciao", " Mondo")); // "Ciao Mondo"

Polimorfismo in Tempo di Esecuzione (Override)

Avviene quando un metodo in una classe derivata ha la stessa firma di un metodo nella classe base,
ma un’implementazione diversa.

Method Override – Polimorfismo Runtime
// Utilizzo del polimorfismo
Veicolo v1 = new Veicolo("Generica", "Base", 2020);
Veicolo v2 = new Automobile("Tesla", "Model 3", 2021, 4, "Elettrica");

v1.Descrivi(); 
// Output: Veicolo: Generica Base, Anno: 2020

v2.Descrivi(); 
// Output: 
// Veicolo: Tesla Model 3, Anno: 2021
// Porte: 4, Alimentazione: Elettrica

// Anche se v2 è di tipo Veicolo, chiama il metodo di Automobile!
// Questo è il polimorfismo in azione

Tipo Polimorfismo Quando Avviene Esempio
Overloading (Compile-time) Stessa classe, parametri diversi Somma(int, int) vs Somma(double, double)
Override (Runtime) Classe derivata sovrascrive metodo base Automobile.Descrivi() sovrascrive Veicolo.Descrivi()

🔌 Interfacce

Un’interfaccia è un contratto che definisce un insieme di membri (metodi, proprietà, eventi)
che una classe deve implementare. A differenza dell’ereditarietà, una classe può implementare
più interfacce.

Definizione e Implementazione Interfacce
// Definizione di un'interfaccia
public interface IPagabile 
{
    decimal CalcolaPagamento();
    DateTime DataScadenza { get; }
}

// Altra interfaccia
public interface IStampabile 
{
    string GeneraDocumento();
}

// Classe che implementa ENTRAMBE le interfacce
public class Fattura : IPagabile, IStampabile 
{
    public int Numero { get; set; }
    public decimal Importo { get; set; }
    public DateTime DataEmissione { get; set; }
    
    public Fattura(int numero, decimal importo) 
    {
        Numero = numero;
        Importo = importo;
        DataEmissione = DateTime.Now;
    }
    
    // Implementazione di IPagabile
    public decimal CalcolaPagamento() 
    {
        return Importo;
    }
    
    public DateTime DataScadenza 
    {
        get { return DataEmissione.AddDays(30); } // Scadenza a 30 giorni
    }
    
    // Implementazione di IStampabile
    public string GeneraDocumento() 
    {
        return $"FATTURA N. {Numero}\n" +
               $"Data: {DataEmissione.ToShortDateString()}\n" +
               $"Importo: {Importo:C}";
    }
}

Altra Classe con Stesse Interfacce
public class Stipendio : IPagabile, IStampabile 
{
    public string Dipendente { get; set; }
    public decimal ImportoLordo { get; set; }
    public decimal Ritenute { get; set; }
    
    // Implementazione di IPagabile
    public decimal CalcolaPagamento() 
    {
        return ImportoLordo - Ritenute;
    }
    
    public DateTime DataScadenza 
    {
        get { return new DateTime(DateTime.Now.Year, DateTime.Now.Month, 27); }
    }
    
    // Implementazione di IStampabile
    public string GeneraDocumento() 
    {
        return $"BUSTA PAGA\n" +
               $"Dipendente: {Dipendente}\n" +
               $"Netto: {CalcolaPagamento():C}";
    }
}

🎯 Vantaggi delle Interfacce

Flessibilità

  • Una classe può implementare più interfacce
  • Definiscono “cosa” fare, non “come”
  • Facilitano il polimorfismo
Contratto

  • Garantiscono comportamenti comuni
  • Documentano le capacità di una classe
  • Facilitano il testing

Utilizzo Polimorfismo con Interfacce
// Lista di oggetti IPagabile
List pagamenti = new List();
pagamenti.Add(new Fattura(1, 1500m));
pagamenti.Add(new Stipendio { Dipendente = "Mario", ImportoLordo = 2000m, Ritenute = 500m });

// Grazie al polimorfismo, possiamo trattarli allo stesso modo
foreach (var p in pagamenti) 
{
    Console.WriteLine($"Pagamento: {p.CalcolaPagamento():C}");
    Console.WriteLine($"Scadenza: {p.DataScadenza:dd/MM/yyyy}");
    Console.WriteLine();
}

📝 Interfacce vs Ereditarietà
  • Ereditarietà: Relazione “è un” (un Cane “è un” Animale)
  • Interfacce: Relazione “può fare” (un Cane “può fare” il verso)
  • Una classe eredita da una sola classe ma può implementare molte interfacce

🎨 Classi e Metodi Astratti

Una classe astratta è una classe che non può essere istanziata direttamente e
che può contenere metodi astratti (metodi senza implementazione). Le classi derivate devono
fornire un’implementazione per tutti i metodi astratti.

Classe Astratta e Metodi Astratti
// Classe astratta
public abstract class Forma 
{
    // Proprietà normali
    public string Colore { get; set; }
    
    // Costruttore
    public Forma(string colore) 
    {
        Colore = colore;
    }
    
    // Metodo concreto (con implementazione)
    public void Descrivi() 
    {
        Console.WriteLine($"Forma di colore {Colore}");
    }
    
    // Metodi astratti (senza implementazione)
    // Le classi derivate DEVONO implementarli
    public abstract double CalcolaArea();
    public abstract double CalcolaPerimetro();
}

Classi Derivate – Rettangolo e Cerchio
// Classe derivata che implementa i metodi astratti
public class Rettangolo : Forma 
{
    public double Larghezza { get; set; }
    public double Altezza { get; set; }
    
    public Rettangolo(double larghezza, double altezza, string colore) 
        : base(colore) 
    {
        Larghezza = larghezza;
        Altezza = altezza;
    }
    
    // Implementazione dei metodi astratti
    public override double CalcolaArea() 
    {
        return Larghezza * Altezza;
    }
    
    public override double CalcolaPerimetro() 
    {
        return 2 * (Larghezza + Altezza);
    }
}

// Altra classe derivata
public class Cerchio : Forma 
{
    public double Raggio { get; set; }
    
    public Cerchio(double raggio, string colore) 
        : base(colore) 
    {
        Raggio = raggio;
    }
    
    public override double CalcolaArea() 
    {
        return Math.PI * Raggio * Raggio;
    }
    
    public override double CalcolaPerimetro() 
    {
        return 2 * Math.PI * Raggio;
    }
}

Utilizzo Polimorfismo con Classi Astratte
// Polimorfismo con classi astratte
List forme = new List();
forme.Add(new Rettangolo(5, 3, "Rosso"));
forme.Add(new Cerchio(4, "Blu"));

foreach (var forma in forme) 
{
    forma.Descrivi();
    Console.WriteLine($"Area: {forma.CalcolaArea():F2}");
    Console.WriteLine($"Perimetro: {forma.CalcolaPerimetro():F2}");
    Console.WriteLine();
}

// Output:
// Forma di colore Rosso
// Area: 15,00
// Perimetro: 16,00
//
// Forma di colore Blu
// Area: 50,27
// Perimetro: 25,13

Caratteristica Classe Astratta Interfaccia
Può avere implementazione? ✅ Sì (metodi concreti e astratti) ❌ No (solo firma)
Può avere costruttore? ✅ Sì ❌ No
Può avere campi? ✅ Sì ❌ No
Ereditarietà multipla? ❌ No (una sola classe base) ✅ Sì (più interfacce)
Quando usarla? Classi strettamente correlate Comportamenti comuni non correlati

🔒 Modificatori sealed e override

Il Modificatore sealed

Il modificatore sealed può essere applicato a classi o metodi. Una classe
sealed non può essere ereditata, mentre un metodo sealed non può
essere sovrascritto nelle classi derivate.

Classe e Metodo sealed
// Classe che NON può essere ereditata
public sealed class ConfigurazioneApp 
{
    public string NomeApp { get; set; }
    public string Versione { get; set; }
    
    // Implementazione...
}

// ❌ ERRORE - Non si può ereditare da una classe sealed
// public class MiaConfigurazione : ConfigurazioneApp { }


// Metodo sealed
public class Veicolo 
{
    public virtual void Avvia() 
    {
        Console.WriteLine("Avvio veicolo...");
    }
}

public class Auto : Veicolo 
{
    // Metodo che non può essere sovrascritto ulteriormente
    public sealed override void Avvia() 
    {
        Console.WriteLine("Avvio automobile...");
    }
}

public class AutoSportiva : Auto 
{
    // ❌ ERRORE - Non si può fare override di un metodo sealed
    // public override void Avvia() { }
}

Il Modificatore override

Il modificatore override è usato per sovrascrivere un metodo virtuale,
astratto o un metodo di interfaccia ereditato dalla classe base.

Esempio Completo con override
public class Animale 
{
    public virtual void FaiVerso() 
    {
        Console.WriteLine("Verso generico");
    }
}

public class Cane : Animale 
{
    public override void FaiVerso() 
    {
        Console.WriteLine("Bau bau!");
    }
}

public class Gatto : Animale 
{
    public override void FaiVerso() 
    {
        Console.WriteLine("Miao miao!");
    }
}

// Utilizzo
Animale a1 = new Cane();
Animale a2 = new Gatto();

a1.FaiVerso(); // Output: Bau bau!
a2.FaiVerso(); // Output: Miao miao!

📌 Riepilogo Modificatori

virtual

  • Permette override nelle derivate
  • Fornisce implementazione default
  • Usato nella classe base
sealed

  • Impedisce ereditarietà/override
  • Migliora performance
  • Usato per “bloccare” la gerarchia

🚀 Esempio Completo: Sistema di Gestione Dipendenti

Vediamo un esempio completo che integra tutti i concetti avanzati di OOP: ereditarietà,
polimorfismo, interfacce e classi astratte.

Interfaccia e Classe Base Astratta
using System;
using System.Collections.Generic;

// Interfaccia per oggetti che possono ricevere un pagamento
public interface IPagabile 
{
    decimal CalcolaPagamento();
    string GetInfoPagamento();
}

// Classe base astratta per tutti i dipendenti
public abstract class Dipendente 
{
    public string Nome { get; set; }
    public string Cognome { get; set; }
    public string CodiceFiscale { get; set; }
    
    // Costruttore
    public Dipendente(string nome, string cognome, string codiceFiscale) 
    {
        Nome = nome;
        Cognome = cognome;
        CodiceFiscale = codiceFiscale;
    }
    
    // Metodo concreto
    public string GetNomeCompleto() 
    {
        return $"{Nome} {Cognome}";
    }
    
    // Metodo astratto che deve essere implementato dalle classi derivate
    public abstract string GetDettagli();
}

Classi Derivate – Dipendenti
// Classe per dipendenti a tempo pieno
public class DipendenteFullTime : Dipendente, IPagabile 
{
    public decimal StipendioAnnuale { get; set; }
    
    public DipendenteFullTime(string nome, string cognome, 
                             string codiceFiscale, decimal stipendioAnnuale)
        : base(nome, cognome, codiceFiscale) 
    {
        StipendioAnnuale = stipendioAnnuale;
    }
    
    public override string GetDettagli() 
    {
        return $"{GetNomeCompleto()} - Full Time - Stipendio: {StipendioAnnuale:C}";
    }
    
    // Implementazione dell'interfaccia IPagabile
    public decimal CalcolaPagamento() 
    {
        return StipendioAnnuale / 12; // Stipendio mensile
    }
    
    public string GetInfoPagamento() 
    {
        return $"Bonifico bancario - {CalcolaPagamento():C} - {GetNomeCompleto()}";
    }
}

// Classe per dipendenti a ore
public class DipendenteOrario : Dipendente, IPagabile 
{
    public decimal TariffaOraria { get; set; }
    public int OreLavorate { get; set; }
    
    public DipendenteOrario(string nome, string cognome, 
                           string codiceFiscale, decimal tariffaOraria)
        : base(nome, cognome, codiceFiscale) 
    {
        TariffaOraria = tariffaOraria;
        OreLavorate = 0;
    }
    
    public void AggiungiOre(int ore) 
    {
        if (ore > 0) 
        {
            OreLavorate += ore;
        }
    }
    
    public override string GetDettagli() 
    {
        return $"{GetNomeCompleto()} - Orario - Tariffa: {TariffaOraria:C}/h - Ore: {OreLavorate}";
    }
    
    // Implementazione dell'interfaccia IPagabile
    public decimal CalcolaPagamento() 
    {
        decimal pagamento = TariffaOraria * OreLavorate;
        
        // Bonus per straordinari
        if (OreLavorate > 40) 
        {
            int oreStraordinario = OreLavorate - 40;
            pagamento += oreStraordinario * TariffaOraria * 0.5m; // +50%
        }
        
        OreLavorate = 0; // Reset delle ore dopo il pagamento
        return pagamento;
    }
    
    public string GetInfoPagamento() 
    {
        return $"Pagamento contanti - {CalcolaPagamento():C} - {GetNomeCompleto()}";
    }
}

Classe Fornitore e Gestore Pagamenti
// Classe per i fornitori esterni (non dipendenti)
public class Fornitore : IPagabile 
{
    public string RagioneSociale { get; set; }
    public string PartitaIVA { get; set; }
    public decimal ImportoFattura { get; set; }
    
    public Fornitore(string ragioneSociale, string partitaIVA, decimal importoFattura) 
    {
        RagioneSociale = ragioneSociale;
        PartitaIVA = partitaIVA;
        ImportoFattura = importoFattura;
    }
    
    public string GetDettagli() 
    {
        return $"{RagioneSociale} - P.IVA: {PartitaIVA} - Fattura: {ImportoFattura:C}";
    }
    
    // Implementazione dell'interfaccia IPagabile
    public decimal CalcolaPagamento() 
    {
        return ImportoFattura;
    }
    
    public string GetInfoPagamento() 
    {
        return $"Bonifico a 60gg - {CalcolaPagamento():C} - {RagioneSociale}";
    }
}

// Classe per gestire i pagamenti
public class GestorePagamenti 
{
    private List pagamenti = new List();
    
    public void AggiungiPagamento(IPagabile pagabile) 
    {
        pagamenti.Add(pagabile);
    }
    
    public decimal CalcolaTotale() 
    {
        decimal totale = 0;
        foreach (var p in pagamenti) 
        {
            totale += p.CalcolaPagamento();
        }
        return totale;
    }
    
    public void ProcessaTuttiPagamenti() 
    {
        Console.WriteLine("╔════════════════════════════════════════════╗");
        Console.WriteLine("║     PROCESSAMENTO PAGAMENTI                ║");
        Console.WriteLine("╚════════════════════════════════════════════╝\n");
        
        foreach (var pagabile in pagamenti) 
        {
            Console.WriteLine($"✅ {pagabile.GetInfoPagamento()}");
        }
        
        Console.WriteLine($"\n💰 Totale da pagare: {CalcolaTotale():C}");
        pagamenti.Clear();
    }
}

Program.cs – Programma Principale
class Program 
{
    static void Main() 
    {
        // Creazione dipendenti e fornitori
        var dipendente1 = new DipendenteFullTime(
            "Mario", "Rossi", "RSSMRA80A01H501R", 50000m);
            
        var dipendente2 = new DipendenteOrario(
            "Luigi", "Verdi", "VRDLGU81B02H502S", 25m);
        dipendente2.AggiungiOre(45);
        
        var fornitore = new Fornitore("Acme SRL", "12345678901", 1200m);
        
        // Utilizzo del polimorfismo con classe base
        Console.WriteLine("═══ ELENCO DIPENDENTI ═══\n");
        List dipendenti = new List { dipendente1, dipendente2 };
        
        foreach (var dip in dipendenti) 
        {
            Console.WriteLine(dip.GetDettagli());
        }
        
        // Gestione pagamenti con interfaccia IPagabile
        Console.WriteLine("\n");
        var gestorePagamenti = new GestorePagamenti();
        gestorePagamenti.AggiungiPagamento(dipendente1);
        gestorePagamenti.AggiungiPagamento(dipendente2);
        gestorePagamenti.AggiungiPagamento(fornitore);
        
        gestorePagamenti.ProcessaTuttiPagamenti();
    }
}

💡 Concetti Dimostrati nell’Esempio
  • Classe astratta: Dipendente è astratta con metodo GetDettagli()
  • Ereditarietà: DipendenteFullTime e DipendenteOrario ereditano da Dipendente
  • Interfaccia: Tutte le classi implementano IPagabile
  • Polimorfismo: Lista di Dipendente e lista di IPagabile

💪 Esercizi Pratici

📝 Esercizio 1: Sistema Biblioteca

Crea un sistema di gestione biblioteca con:

  • Classe astratta ElementoBiblioteca con titolo, anno
  • Classi derivate: Libro (autore, pagine), Rivista (numero, mese)
  • Interfaccia IPrestabile con metodi per prestito/restituzione
  • Lista polimorfica di elementi

🎯 Riepilogo Lezione

Hai imparato:

  • Ereditarietà e relazione “è un”
  • Polimorfismo (overloading e override)
  • Interfacce per contratti
  • Classi astratte
  • Modificatori sealed
Concetti chiave:

  • virtual/override per polimorfismo
  • abstract per metodi obbligatori
  • Interfacce per ereditarietà multipla
  • sealed per bloccare gerarchia
  • base per chiamare classe padre

Lezione 13 del Corso C# Completo

Torna in alto