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
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;
}
}
- Metodo che può essere sovrascritto
- Nella classe base
- Fornisce implementazione di default
- Sovrascrive metodo virtuale
- Nella classe derivata
- Può chiamare
base.Metodo()
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.
// 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.
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.
// 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 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}";
}
}
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}";
}
}
- Una classe può implementare più interfacce
- Definiscono “cosa” fare, non “come”
- Facilitano il polimorfismo
- Garantiscono comportamenti comuni
- Documentano le capacità di una classe
- Facilitano il testing
// Lista di oggetti IPagabile Listpagamenti = 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(); }
- 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
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();
}
// 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;
}
}
// Polimorfismo con classi astratte Listforme = 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 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.
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!
- Permette override nelle derivate
- Fornisce implementazione default
- Usato nella classe base
- 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.
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();
}
// 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 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();
}
}
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();
}
}
- Classe astratta:
Dipendenteè astratta con metodoGetDettagli() - Ereditarietà:
DipendenteFullTimeeDipendenteOrarioereditano daDipendente - Interfaccia: Tutte le classi implementano
IPagabile - Polimorfismo: Lista di
Dipendentee lista diIPagabile
💪 Esercizi Pratici
📝 Esercizio 1: Sistema Biblioteca
Crea un sistema di gestione biblioteca con:
- Classe astratta
ElementoBibliotecacon titolo, anno - Classi derivate:
Libro(autore, pagine),Rivista(numero, mese) - Interfaccia
IPrestabilecon metodi per prestito/restituzione - Lista polimorfica di elementi
- Ereditarietà e relazione “è un”
- Polimorfismo (overloading e override)
- Interfacce per contratti
- Classi astratte
- Modificatori sealed
- 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