1. Interfacce nella business logic: bottoni e azioni
Questa è l'ultima lezione sulle interfacce, quindi qui ci saranno un sacco di esempi pratici per farti capire meglio come si usano davvero le interfacce.
Le interfacce sono particolarmente preziose nelle app grandi. Quindi sotto trovi casi complicati, a volte anche un po' avanti rispetto al programma del corso. Se ti interessa — top! Se no — passa pure al prossimo livello :P
L'esempio più semplice — bottoni e click su di essi
Continuiamo a sviluppare la nostra app console didattica — immaginiamo di avere un sistema semplice di elementi UI (tipo bottoni, campi di testo e switch) in un menu console.
Vogliamo che alcuni elementi reagiscano al click (bottoni), altri no (tipo un titolo statico). Le interfacce ti permettono di realizzare questi scenari in modo elegante.
Step 1: Creiamo l'interfaccia
// Componente che può essere "cliccato":
public interface IClickable
{
void Click();
}
Step 2: Applichiamo l'interfaccia a diversi tipi di elementi
// Classe base per tutti gli elementi del menu
public class MenuItem
{
public string Title { get; set; }
public MenuItem(string title)
{
Title = title;
}
public virtual void Display()
{
Console.WriteLine(Title);
}
}
// Bottone: può essere "cliccato"
public class Button : MenuItem, IClickable
{
public Button(string title) : base(title) {}
public void Click()
{
Console.WriteLine($"[Bottone] {Title} è stato premuto!");
}
}
// Solo etichetta: non si può cliccare
public class Label : MenuItem
{
public Label(string title) : base(title) {}
}
Step 3: Usiamo il polimorfismo dell'interfaccia
IClickable[] clickableItems = new IClickable[]
{
new Button("Salva"),
new Button("Esci")
// Label qui non si può aggiungere — non è IClickable!
};
foreach (var item in clickableItems)
{
item.Click();
}
Vedi la magia? La lista accetta solo chi “sa” Click, quindi non avrai errori a runtime se provi a “cliccare” su un oggetto sbagliato.
2. Interfacce e pattern “Strategia”: scegliere l’algoritmo al volo
Supponiamo che tu stia scrivendo un'app dove puoi salvare i report in modi diversi: su file, su database, nel “cloud”, o magari anche su Telegram al tuo capo (non chiedere, succede di tutto).
Step 1: Formuliamo il problema
Serve che la classe ReportGenerator possa lavorare con qualsiasi modo di salvataggio, senza sapere i dettagli dell’implementazione.
Step 2: Descriviamo l’interfaccia-strategia
public interface IDataSaver
{
void Save(string reportData);
}
Step 3: Implementiamo le varie opzioni
public class FileDataSaver : IDataSaver
{
public void Save(string reportData)
{
Console.WriteLine("[FileDataSaver] Salvo su file...\n" + reportData);
// Qui ci potrebbe essere il codice per File.WriteAllText(...)
}
}
public class DatabaseDataSaver : IDataSaver
{
public void Save(string reportData)
{
Console.WriteLine("[DatabaseDataSaver] Salvo su database...\n" + reportData);
// Qui ci potrebbe essere il codice per scrivere sul database
}
}
Step 4: Usiamo l’interfaccia per cambiare strategia di salvataggio
public class ReportGenerator
{
private readonly IDataSaver _dataSaver;
public ReportGenerator(IDataSaver dataSaver)
{
_dataSaver = dataSaver;
}
public void GenerateReport()
{
string report = "Questo è un report importante!";
Console.WriteLine("Genero il report...");
_dataSaver.Save(report);
}
}
Dimostrazione della flessibilità
// Puoi cambiare il modo di salvare senza riscrivere ReportGenerator!
IDataSaver fileSaver = new FileDataSaver();
ReportGenerator fileReport = new ReportGenerator(fileSaver);
fileReport.GenerateReport();
IDataSaver dbSaver = new DatabaseDataSaver();
ReportGenerator dbReport = new ReportGenerator(dbSaver);
dbReport.GenerateReport();
Utilità pratica: Se domani vuoi scrivere i report su un nuovo servizio supermoderno, ti basta scrivere una nuova classe che implementa l’interfaccia e infilare l’oggetto nella tua architettura, senza cambiare una riga di codice esistente.
3. Interfacce in .NET: IDisposable e using
Una delle interfacce più usate in .NET è IDisposable. La implementano tutte le classi che lavorano con risorse non gestite: file, stream, connessioni di rete.
Perché serve IDisposable?
Quando lavori con una risorsa che va liberata per forza (tipo chiudere un file), implementi IDisposable e scrivi il metodo Dispose. Così puoi passare questi oggetti all’operatore using, che garantisce la chiamata di Dispose quando esci dal blocco.
Esempio: Simulazione di lavoro con un file
public class FakeFile : IDisposable
{
public string FileName { get; }
public FakeFile(string fileName)
{
FileName = fileName;
Console.WriteLine($"Aperto file: {fileName}");
}
public void Dispose()
{
Console.WriteLine($"Chiuso file: {FileName}");
}
}
// Nel programma principale:
using (var file = new FakeFile("report.txt"))
{
Console.WriteLine("Scriviamo sul file...");
// il file si "chiuderà" automaticamente dopo using
}
Output:
Aperto file: report.txt
Scriviamo sul file...
Chiuso file: report.txt
Davvero utile: non ti dimenticherai di chiudere il file — l’interfaccia e l’infrastruttura del linguaggio ti proteggono.
4. Interfacce nelle collezioni e LINQ
Quando lavori con liste, array, dizionari, stai già usando le interfacce, anche se magari non ci pensi.
List<int> list = new List<int> { 1, 2, 3 };
IEnumerable<int> enumerable = list; // tutto ok!
// Ora puoi ciclare gli elementi così:
foreach(var x in enumerable)
{
Console.WriteLine(x);
}
La maggior parte dei metodi LINQ lavora con le collezioni tramite l’interfaccia IEnumerable<T>. Così puoi scrivere codice che non dipende dal tipo concreto di collezione.
Perché serve?
Puoi sostituire List<T> con T[], HashSet<T> o anche una tua collezione custom — e il tuo codice continuerà a funzionare!
5. Interfacce per il testing (Mock object)
Nei test è fondamentale isolare il codice dalle dipendenze esterne: niente database veri, niente file reali. Le interfacce ti permettono di usare stub (mock object) e testare il codice senza toccare database o file veri.
public class FakeDataSaver : IDataSaver
{
public bool WasCalled { get; private set; } = false;
public void Save(string reportData)
{
WasCalled = true;
Console.WriteLine("Dati salvati nel mock object!");
}
}
// Nel test
FakeDataSaver saver = new FakeDataSaver();
ReportGenerator generator = new ReportGenerator(saver);
generator.GenerateReport();
Console.WriteLine($"Save è stato chiamato? {saver.WasCalled}");
Risultato: il test non dipende dal mondo reale, ma verifica che il metodo giusto sia stato chiamato!
6. Implementazione esplicita delle interfacce per risolvere conflitti
A volte una classe deve implementare due interfacce con metodi uguali, ma la logica di questi metodi è diversa.
public interface IFlyable
{
void Move();
}
public interface ISwimmable
{
void Move();
}
public class Duck : IFlyable, ISwimmable
{
// Implementazione esplicita
void IFlyable.Move()
{
Console.WriteLine("L'anatra vola!");
}
void ISwimmable.Move()
{
Console.WriteLine("L'anatra nuota!");
}
}
Duck duck = new Duck();
// duck.Move(); // Errore: non esiste questo metodo!
((IFlyable)duck).Move(); // "L'anatra vola!"
((ISwimmable)duck).Move(); // "L'anatra nuota!"
Sembra un po' hardcore, ma a volte è proprio quello che chiedono le specifiche!
7. Interfacce nel modello event-driven: INotifyPropertyChanged
In .NET ci sono interfacce standard per supportare gli eventi — tipo quando nei modelli dati cambia qualcosa e la UI deve saperlo (spesso in WPF, MAUI e altri GUI).
using System.ComponentModel;
public class Person : INotifyPropertyChanged
{
private string name;
public string Name
{
get => name;
set
{
if (name != value)
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
Il senso: qualsiasi framework che supporta il data binding si aspetta che la tua classe implementi questa interfaccia — così la UI si aggiornerà automaticamente quando cambiano le proprietà.
8. Interfacce nel pattern “Factory”
Le interfacce sono perfette per costruire factory — classi che creano oggetti diversi ma intercambiabili.
public interface ITransport
{
void Move();
}
public class Bicycle : ITransport
{
public void Move() => Console.WriteLine("Andiamo in bici!");
}
public class Car : ITransport
{
public void Move() => Console.WriteLine("Andiamo in macchina!");
}
public class TransportFactory
{
public static ITransport Create(string type)
{
return type switch
{
"bike" => new Bicycle(),
"car" => new Car(),
_ => throw new ArgumentException("Tipo di trasporto sconosciuto")
};
}
}
ITransport transport = TransportFactory.Create("bike");
transport.Move(); // "Andiamo in bici!"
9. Interfacce per eventi: esempio di evento custom
Descriviamo l’interfaccia del “listener di evento”:
public interface ILoginListener
{
void OnLogin(string userName);
}
// Classe che scatena l’evento
public class LoginManager
{
private List<ILoginListener> listeners = new();
public void Subscribe(ILoginListener listener) => listeners.Add(listener);
public void Login(string userName)
{
Console.WriteLine($"Utente {userName} è entrato.");
foreach (var listener in listeners)
listener.OnLogin(userName);
}
}
// Classe che implementa l’interfaccia
public class WelcomeMessage : ILoginListener
{
public void OnLogin(string userName)
{
Console.WriteLine($"Benvenuto, {userName}!");
}
}
LoginManager manager = new();
manager.Subscribe(new WelcomeMessage());
manager.Login("Vasya");
// Utente Vasya è entrato.
// Benvenuto, Vasya!
Al colloquio: se ti chiedono — “come implementeresti un sistema di eventi custom?”, racconta pure delle interfacce-listener!
10. Interfacce per plugin (estendibilità delle app)
Molte app grandi supportano i plugin. Grazie alle interfacce la tua app può “caricare” nuovi moduli al volo, senza sapere nulla dei loro dettagli interni.
public interface IPlugin
{
string Name { get; }
void Run();
}
// La tua app:
public class PluginLoader
{
public void LoadAndRun(IEnumerable<IPlugin> plugins)
{
foreach (var plugin in plugins)
{
Console.WriteLine($"Avvio plugin: {plugin.Name}");
plugin.Run();
}
}
}
I plugin possono essere sviluppati da terzi — l’importante è che implementino l’interfaccia. La tua app diventa estendibile!
GO TO FULL VERSION