Nel mondo dello sviluppo moderno, la capacità di scrivere codice riutilizzabile, flessibile e facilmente testabile è un tratto distintivo degli sviluppatori senior. Architetture solide si basano su due principi cardine:
- Separation of concerns
- Inversion of Control (IoC)
In questo articolo esploreremo come usare Dependency Injection (DI) e IoC in modo avanzato, applicando pattern flessibili e reali in ambienti enterprise complessi.
1. Dependency Injection e IoC: Oltre le Basi
L’idea chiave è semplice: le dipendenze non devono essere istanziate all’interno della classe, ma fornite dall’esterno. Questo permette maggiore flessibilità, estendibilità e testabilità.
Esempio base:
csharpCopiaModificapublic interface IReportService
{
void Generate();
}
public class PdfReportService : IReportService
{
public void Generate() => Console.WriteLine("Generazione report PDF...");
}
public class ReportManager
{
private readonly IReportService _service;
public ReportManager(IReportService service) => _service = service;
public void Process() => _service.Generate();
}
Vantaggi:
- Facilità di test (mocking, sostituzioni)
- Codice aperto a estensioni, chiuso a modifiche (principio OCP)
- Low coupling tra classi
2. Uso di un IoC Container
In contesti reali, l’iniezione manuale è inefficiente. Un IoC container (come quello di ASP.NET Core) gestisce automaticamente la creazione e la risoluzione delle dipendenze.
Registrazione:
csharpCopiaModificaservices.AddScoped<IReportService, PdfReportService>();
services.AddScoped<ReportManager>();
Risoluzione:
csharpCopiaModificavar manager = serviceProvider.GetRequiredService<ReportManager>();
manager.Process();
👉 Scope disponibili:
Transient
: nuova istanza ogni voltaScoped
: una per richiestaSingleton
: una per tutta l’applicazione
3. Binding Dinamico a Runtime
Spesso il comportamento dell’applicazione deve cambiare in base al contesto.
Soluzione: Factory con IoC
csharpCopiaModificapublic class ReportFactory
{
private readonly IServiceProvider _provider;
public ReportFactory(IServiceProvider provider) => _provider = provider;
public IReportService Create(string type)
{
return type switch
{
"pdf" => _provider.GetRequiredService<PdfReportService>(),
"html" => _provider.GetRequiredService<HtmlReportService>(),
_ => throw new NotSupportedException()
};
}
}
Questo approccio centralizza la logica e rende le decisioni configurabili.
4. Decorator Pattern con DI
Hai bisogno di aggiungere funzionalità (come logging, caching) senza modificare l’implementazione originale? Usa un Decorator.
Esempio:
csharpCopiaModificapublic class LoggingReportService : IReportService
{
private readonly IReportService _inner;
public LoggingReportService(IReportService inner) => _inner = inner;
public void Generate()
{
Console.WriteLine("LOG: Inizio...");
_inner.Generate();
Console.WriteLine("LOG: Fine.");
}
}
Registrazione:
csharpCopiaModificaservices.AddScoped<PdfReportService>();
services.AddScoped<IReportService>(provider =>
new LoggingReportService(provider.GetRequiredService<PdfReportService>())
);
5. Iniezione di Collezioni: Strategie Plugin-Based
Se hai più implementazioni della stessa interfaccia, puoi iniettarle tutte con IEnumerable<T>
.
csharpCopiaModificapublic class CompositeReportService : IReportService
{
private readonly IEnumerable<IReportService> _services;
public CompositeReportService(IEnumerable<IReportService> services) => _services = services;
public void Generate()
{
foreach (var service in _services)
service.Generate();
}
}
👉 Ottimo per pattern plugin e strategie di estensione modulari.
6. Feature Toggle e Strategie Condizionali
In ambienti complessi o multi-tenant è utile gestire configurazioni dinamiche tramite feature toggle.
csharpCopiaModificaservices.AddScoped<IReportService>(provider =>
{
var config = provider.GetRequiredService<IOptions<AppSettings>>().Value;
return config.UsePdf
? provider.GetRequiredService<PdfReportService>()
: provider.GetRequiredService<HtmlReportService>();
});
7. Testing Avanzato con Mock
Una delle vere forze di DI è la facilità di testing. Con librerie come Moq:
csharpCopiaModificavar mock = new Mock<IReportService>();
mock.Setup(s => s.Generate()).Verifiable();
var manager = new ReportManager(mock.Object);
manager.Process();
mock.Verify(); // Verifica che sia stato chiamato
Test rapidi, isolati e affidabili.
8. Anti-Pattern da Evitare
- ❌ Service Locator: rende le dipendenze nascoste e poco testabili.
- ❌ Over-injection: se un costruttore ha troppe dipendenze, la classe è probabilmente violando il principio Single Responsibility.
- ❌ Injection diretta di
IServiceProvider
: va bene solo in scenari controllati, come factory delegate. - ❌ Costruttori pesanti: mantienili leggeri, senza logica.
Conclusione
Dependency Injection e Inversion of Control sono strumenti essenziali per costruire architetture moderne, scalabili e mantenibili. Con pattern avanzati come decorator, factory dinamiche, plugin e feature toggle, è possibile progettare sistemi altamente adattabili senza sacrificare la semplicità del codice.
Un sistema ben progettato non solo funziona oggi, ma è pronto per evolversi domani.