En tres lliçons diferents del Mòdul 7 —ASP.NET Core, Blazor i MAUI— va aparèixer el mateix
missatge de passada: "això és injecció de dependències, s'estudiarà en profunditat al Mòdul 8".
Ha arribat aquest moment. Aquesta lliçó explica primer el problema de fons que resol la
inversió de control (acoblar codi a implementacions concretes dificulta el canvi i, sobretot,
dificulta les proves), després compara les diferents formes d'injectar una dependència, i per
últim aplica tot això al cas més concret de BiblioTech: extreure una interfície
IRepositoriBiblioteca a partir de les quatre formes de persistència construïdes al Mòdul 5
(text, JSON, SQLite, Entity Framework Core), de manera que Biblioteca pugui fer servir
qualsevol d'elles sense saber quina està fent servir en cada moment. De passada, s'explica en
detall el contenidor de serveis d'ASP.NET Core que ja vas fer servir, sense aprofundir-hi, a la
lliçó de Minimal APIs.
Contingut
- El problema: acoblament fort i dificultat per provar
- La inversió de control com a solució
- Injecció per constructor davant de per propietat o mètode
- Extraient
IRepositoriBibliotecadels repositoris del Mòdul 5 Bibliotecainjectada ambIRepositoriBiblioteca- Contenidors de DI en profunditat:
AddSingleton/AddScoped/AddTransient - Exemple complet: registrar i resoldre a ASP.NET Core
- El problema: acoblament fort i dificultat per provar
Imagina que Biblioteca desa i carrega el seu catàleg cridant directament els mètodes de
persistència en SQLite construïts al Mòdul 5:
class Biblioteca
{
public List<MaterialBibliotecari> Cataleg { get; } = new List<MaterialBibliotecari>();
public void DesarCataleg()
{
// Biblioteca "sap" que la persistencia es SQLite, amb detalls concrets de connexio
using SqliteConnection connexio = new SqliteConnection("Data Source=bibliotech.db");
connexio.Open();
// ... SQL d'INSERT/UPDATE, un per cada Llibre del cataleg ...
}
}Aquest disseny té dos problemes seriosos, encara que el codi "funcioni":
- Acoblament fort:
Bibliotecaqueda lligada a SQLite de forma permanent. Canviar a JSON, o a Entity Framework, o a un tercer sistema de persistència futur, obliga a modificarBibliotecadirectament —una classe que, en principi, només hauria de preocupar-se de la lògica de préstecs i catàleg, no dels detalls de com es desen les dades. - Dificultat per provar: per escriure una prova unitària (Lliçó 4 d'aquest mòdul) sobre la
lògica de
Biblioteca, caldria una base de dades SQLite real disponible, amb dades conegudes, cada vegada que s'executessin els tests. Això fa les proves lentes, fràgils (depenen d'un fitxer extern) i difícils d'executar a qualsevol màquina sense preparació prèvia.
- La inversió de control com a solució
La inversió de control (IoC) inverteix qui decideix quina implementació concreta es fa
servir: en comptes que Biblioteca creï directament la seva pròpia dependència de persistència
(new SqliteConnection(...)), la rep ja construïda des de fora, a través d'una abstracció
(una interfície) que no revela els detalls de la implementació concreta:
flowchart LR
subgraph "Sense inversio de control"
B1["Biblioteca"] -->|"new SqliteConnection(...)"| S1["SQLite concret"]
end
subgraph "Amb inversio de control"
B2["Biblioteca"] -->|"depen de"| I["IRepositoriBiblioteca (interficie)"]
S2["RepositoriSqlite"] -.->|"implementa"| I
S3["RepositoriJson"] -.->|"implementa"| I
Ext["Codi extern (Program.cs, un contenidor de DI...)"] -->|"decideix quin crear i l'injecta"| B2
end
Biblioteca deixa de decidir "com" es persisteixen les dades: només declara que necessita
alguna cosa que compleixi el contracte IRepositoriBiblioteca, i qui la construeix decideix
quina implementació concreta li lliura. La injecció de dependències és, precisament, la
tècnica mitjançant la qual aquesta dependència "entra" des de fora —per constructor, propietat o
mètode, com veu l'apartat següent— en comptes de crear-se internament amb new. És la forma més
habitual d'aplicar el principi d'inversió de control.
- Injecció per constructor davant de per propietat o mètode
Existeixen tres formes d'injectar una dependència en una classe:
| Forma | Com es declara | Quan es resol la dependència |
|---|---|---|
| Per constructor | Paràmetre del constructor, assignat a un camp/propietat de només lectura | En crear l'objecte; mai pot quedar en un estat a mig configurar |
| Per propietat | Una propietat pública amb set, assignada després de crear l'objecte |
En qualsevol moment després de la construcció; l'objecte pot existir sense la dependència encara assignada |
| Per mètode | Un paràmetre d'un mètode concret, en comptes d'un membre de la classe | Només en el moment de cridar aquest mètode concret |
// Injeccio per constructor (la recomanada per defecte)
class Biblioteca
{
private readonly IRepositoriBiblioteca _repositori;
public Biblioteca(IRepositoriBiblioteca repositori)
{
_repositori = repositori; // obligatoria: no es pot crear Biblioteca sense ella
}
}
// Injeccio per propietat
class Biblioteca
{
public IRepositoriBiblioteca? Repositori { get; set; } // opcional: pot quedar sense assignar
public void DesarCataleg()
{
Repositori?.GuardarCataleg(Cataleg); // cal comprovar que no sigui null abans de fer-la servir
}
public List<MaterialBibliotecari> Cataleg { get; } = new List<MaterialBibliotecari>();
}
// Injeccio per metode
class ImportadorMaterials
{
public void ImportarDesDe(IRepositoriBiblioteca repositori, Biblioteca biblioteca)
{
biblioteca.Cataleg.AddRange(repositori.CarregarCataleg()); // nomes valida durant aquesta crida
}
}La injecció per constructor és la pràctica recomanada per defecte en la immensa majoria de
casos, per dues raons: fa la dependència obligatòria (no es pot crear una Biblioteca sense
proporcionar el seu repositori, cosa que elimina d'arrel qualsevol NullReferenceException per
oblit), i fa explícit, només mirant la signatura del constructor, de què depèn exactament la
classe. La injecció per propietat es reserva per a dependències genuïnament opcionals (per
exemple, un servei de registre/logging que pot faltar sense que l'objecte deixi de funcionar);
la injecció per mètode, per a dependències que només calen durant una operació puntual, no
durant tota la vida de l'objecte.
- Extraient
IRepositoriBiblioteca dels repositoris del Mòdul 5
IRepositoriBiblioteca dels repositoris del Mòdul 5El Mòdul 5 va construir quatre formes diferents de persistir el catàleg de BiblioTech: text pla
(GuardarCatalegText/CarregarCatalegText), JSON (GuardarEstatJson/CarregarEstatJson),
SQLite amb ADO.NET (GuardarLlibresASqlite/CarregarLlibresDesDeSqlite) i Entity Framework Core
(BibliotecaDbContext). Les quatre resolen, en el fons, la mateixa pregunta: "desa aquest
catàleg" i "recupera'm el catàleg desat". Aquesta és la senyal exacta que cal una interfície
comuna:
interface IRepositoriBiblioteca
{
void GuardarCataleg(List<MaterialBibliotecari> cataleg);
List<MaterialBibliotecari> CarregarCataleg();
}Cada mecanisme del Mòdul 5 passa a ser una classe independent que implementa aquesta interfície, reutilitzant la lògica ja escrita llavors:
class RepositoriText : IRepositoriBiblioteca
{
private readonly string _ruta;
public RepositoriText(string ruta) => _ruta = ruta;
public void GuardarCataleg(List<MaterialBibliotecari> cataleg)
{
using StreamWriter escriptor = new StreamWriter(_ruta); // Modul 5, GuardarCatalegText
foreach (MaterialBibliotecari material in cataleg)
{
escriptor.WriteLine($"{material.Titol}|{material.Autor}|{material.Disponible}");
}
}
public List<MaterialBibliotecari> CarregarCataleg()
{
// mateixa logica que CarregarCatalegText del Modul 5, adaptada per retornar la llista
// en comptes d'omplir directament Biblioteca.Cataleg
List<MaterialBibliotecari> cataleg = new List<MaterialBibliotecari>();
// ... lectura linia a linia amb StreamReader, reconstruint cada Llibre ...
return cataleg;
}
}
class RepositoriJson : IRepositoriBiblioteca
{
private readonly string _ruta;
public RepositoriJson(string ruta) => _ruta = ruta;
public void GuardarCataleg(List<MaterialBibliotecari> cataleg)
{
string json = JsonSerializer.Serialize(cataleg); // Modul 5, GuardarEstatJson
File.WriteAllText(_ruta, json);
}
public List<MaterialBibliotecari> CarregarCataleg()
{
string json = File.ReadAllText(_ruta);
return JsonSerializer.Deserialize<List<MaterialBibliotecari>>(json) ?? new List<MaterialBibliotecari>();
}
}
class RepositoriSqlite : IRepositoriBiblioteca
{
private readonly string _cadenaConnexio;
public RepositoriSqlite(string cadenaConnexio) => _cadenaConnexio = cadenaConnexio;
public void GuardarCataleg(List<MaterialBibliotecari> cataleg)
{
// mateixa logica que GuardarLlibresASqlite del Modul 5, amb SqliteConnection/SqliteCommand
}
public List<MaterialBibliotecari> CarregarCataleg()
{
// mateixa logica que CarregarLlibresDesDeSqlite del Modul 5, amb SqliteDataReader
return new List<MaterialBibliotecari>();
}
}
class RepositoriEntityFramework : IRepositoriBiblioteca
{
private readonly BibliotecaDbContext _context;
public RepositoriEntityFramework(BibliotecaDbContext context) => _context = context;
public void GuardarCataleg(List<MaterialBibliotecari> cataleg)
{
_context.AddRange(cataleg.OfType<Llibre>()); // simplificat: EF Core distingeix Llibre/Revista pel seu tipus
_context.SaveChanges(); // versio sincrona de SaveChangesAsync, Modul 5
}
public List<MaterialBibliotecari> CarregarCataleg()
{
return _context.Llibres.Cast<MaterialBibliotecari>().ToList(); // consulta LINQ, Modul 5
}
}Cap d'aquestes quatre classes modifica la lògica de persistència que ja vas construir al Mòdul 5
—només la reorganitza darrere d'un contracte comú. RepositoriEntityFramework, en particular,
rep el seu propi BibliotecaDbContext també per injecció de constructor: la injecció de
dependències s'aplica en cadena, no només al punt més extern.
Biblioteca injectada amb IRepositoriBiblioteca
Biblioteca injectada amb IRepositoriBibliotecaAmb la interfície ja definida, Biblioteca deixa de conèixer cap detall concret de persistència:
class Biblioteca
{
private readonly IRepositoriBiblioteca _repositori;
public List<MaterialBibliotecari> Cataleg { get; private set; } = new List<MaterialBibliotecari>();
public Biblioteca(IRepositoriBiblioteca repositori)
{
_repositori = repositori;
}
public void GuardarCataleg() => _repositori.GuardarCataleg(Cataleg);
public void CarregarCataleg() => Cataleg = _repositori.CarregarCataleg();
}// Amb RepositoriJson
Biblioteca bibliotecaJson = new Biblioteca(new RepositoriJson("catalogo.json"));
// Exactament la mateixa classe Biblioteca, ara amb SQLite, sense canviar ni una linia de Biblioteca
Biblioteca bibliotecaSqlite = new Biblioteca(new RepositoriSqlite("Data Source=bibliotech.db"));Biblioteca és idèntica en tots dos casos: l'única cosa que canvia és quina implementació
concreta d'IRepositoriBiblioteca se li lliura en construir-la. Aquest és exactament l'objectiu
de la inversió de control plantejat a l'apartat 2, ja resolt de forma concreta sobre el domini de
BiblioTech. A més, com es va anticipar a l'apartat 1, aquesta mateixa estructura és la que
permetrà, a la Lliçó 4, substituir IRepositoriBiblioteca per un doble de prova (mock) sense
necessitar cap fitxer ni base de dades real durant els tests.
- Contenidors de DI en profunditat:
AddSingleton/AddScoped/AddTransient
AddSingleton/AddScoped/AddTransientLa lliçó d'ASP.NET Core (Mòdul 7) va introduir el contenidor de serveis de forma bàsica, amb
builder.Services.AddSingleton<Biblioteca>(). Ara que coneixes la teoria completa de la injecció
de dependències, és el moment d'entendre per què existeixen tres mètodes diferents de
registre, no només AddSingleton:
| Mètode | Quantes instàncies crea | Quan fer-lo servir |
|---|---|---|
AddSingleton<T>() |
Una única instància, compartida durant tota la vida de l'aplicació | Estat veritablement global i segur de compartir entre peticions concurrents (per exemple, IPoliticaSancio de la lliçó anterior, si no desa estat propi) |
AddScoped<T>() |
Una instància nova per cada petició HTTP (o "àmbit"), compartida dins d'aquesta mateixa petició | Dependències amb estat propi d'una operació, com BibliotecaDbContext (recorda l'error comú de la lliçó d'ASP.NET Core: mai com a singleton) |
AddTransient<T>() |
Una instància nova cada vegada que es demana, fins i tot diverses vegades dins de la mateixa petició | Dependències lleugeres, sense estat, que no importa crear repetidament |
var builder = WebApplication.CreateBuilder(args);
// IRepositoriBiblioteca resolt com una instancia de RepositoriEntityFramework
builder.Services.AddScoped<IRepositoriBiblioteca, RepositoriEntityFramework>();
builder.Services.AddScoped<BibliotecaDbContext>(); // una instancia nova per peticio, correcte (Modul 7)
builder.Services.AddScoped<Biblioteca>(); // Biblioteca ara tambe depen de IRepositoriBiblioteca
var app = builder.Build();
app.MapGet("/libros", (Biblioteca biblioteca) =>
{
biblioteca.CarregarCataleg();
return Results.Ok(biblioteca.Cataleg);
});AddScoped<IRepositoriBiblioteca, RepositoriEntityFramework>() registra dos tipus: el
primer és el tipus que es demanarà (la interfície), el segon és la implementació concreta
que el contenidor ha de construir quan algú demani aquest tipus. Quan l'endpoint declara
Biblioteca biblioteca com a paràmetre, ASP.NET Core resol automàticament tota la cadena:
construeix un RepositoriEntityFramework, que al seu torn necessita un BibliotecaDbContext
(també registrat), i amb aquest repositori ja construït, construeix finalment la Biblioteca que
l'endpoint rep. Cap d'aquestes construccions intermèdies l'escriu el programador a mà: el
contenidor de DI la resol llegint els constructors de cada classe registrada.
AddScoped per a Biblioteca (en comptes d'AddSingleton, com es va veure de forma
simplificada al Mòdul 7) és ara l'elecció correcta: com que Biblioteca depèn d'un
BibliotecaDbContext —que ja saps que ha de viure només durant una petició—, Biblioteca
mateixa ha de tenir el mateix cicle de vida que la seva dependència, o arrossegaria un
DbContext obsolet entre peticions diferents.
- Exemple complet: registrar i resoldre a ASP.NET Core
Unint totes les peces anteriors, així queda el registre complet de serveis d'una API de
BiblioTech que fa servir Entity Framework Core com a mecanisme de persistència, amb Biblioteca
injectada amb IRepositoriBiblioteca en comptes d'acoblada directament a EF Core:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<BibliotecaDbContext>(opcions =>
opcions.UseSqlite("Data Source=bibliotech.db")); // AddDbContext ja registra el cicle de vida correcte (Scoped)
builder.Services.AddScoped<IRepositoriBiblioteca, RepositoriEntityFramework>();
builder.Services.AddScoped<Biblioteca>();
var app = builder.Build();
app.MapPost("/catalogo/guardar", (Biblioteca biblioteca) =>
{
biblioteca.GuardarCataleg();
return Results.Ok("Cataleg desat.");
});
app.Run();Per provar amb un altre mecanisme de persistència —per exemple, en un entorn de proves manuals sense base de dades— n'hi hauria prou amb canviar una única línia:
// En comptes d'EF Core, fa servir JSON: ni Biblioteca ni els endpoints necessiten cap canvi
builder.Services.AddSingleton<IRepositoriBiblioteca>(new RepositoriJson("catalogo.json"));Aquest és el resultat pràctic d'haver invertit el control des de l'apartat 2: canviar per complet
el mecanisme de persistència de tota l'API és, ara, una única línia a Program.cs, en comptes
d'una modificació escampada per tot Biblioteca.
Errors Comuns i Consells
- Registrar
Bibliotecacom aAddSingletonquan depèn d'unBibliotecaDbContextScoped: el contenidor d'ASP.NET Core ho detecta i llança una excepció en arrencar (captive dependency, una dependència de vida curta "atrapada" dins d'un objecte de vida més llarga). El cicle de vida d'una classe mai ha de ser més llarg que el de les seves dependències. - Injectar per propietat "per comoditat" en comptes de per constructor: fa que la dependència sembli opcional quan en realitat és obligatòria, i permet crear l'objecte en un estat incomplet (sense la dependència assignada) que només falla més tard, en fer-la servir.
- Confondre la interfície
IRepositoriBibliotecaamb "una capa inútil de més": sembla redundant mentre només hi ha una implementació real, però el seu valor apareix just quan cal canviar d'implementació (apartat 7) o substituir-la per un doble de prova (Lliçó 4). - Consell: si en dissenyar una classe et preguntes "això hauria de ser
newaquí dins, o rebut des de fora?", la pregunta d'or és: alguna vegada necessitaré substituir aquesta dependència per una altra implementació, o per una versió de prova? Si la resposta és sí, injecta-la.
Exercicis
-
Defineix una interfície
INotificadoramb un únic mètodevoid Notificar(string missatge), i dues implementacions:NotificadorConsola(que faci servirConsole.WriteLine) iNotificadorSilencios(que no faci res). ModificaBibliotecaperquè rebi unINotificadorper constructor i el faci servir dins deRegistrarPrestecen comptes de cridar directamentConsole.WriteLine. -
Al registre de serveis d'ASP.NET Core de l'apartat 7, explica per què
IRepositoriBibliotecaiBibliotecas'han de registrar com aAddScoped(i noAddSingleton) mentre depenguin, directament o indirectament, deBibliotecaDbContext.
Solucions
interface INotificador
{
void Notificar(string missatge);
}
class NotificadorConsola : INotificador
{
public void Notificar(string missatge) => Console.WriteLine(missatge);
}
class NotificadorSilencios : INotificador
{
public void Notificar(string missatge) { /* no fa res */ }
}
class Biblioteca
{
private readonly IRepositoriBiblioteca _repositori;
private readonly INotificador _notificador;
public List<Prestec> Prestecs { get; } = new List<Prestec>();
public Biblioteca(IRepositoriBiblioteca repositori, INotificador notificador)
{
_repositori = repositori;
_notificador = notificador;
}
public void RegistrarPrestec(Prestec prestec)
{
Prestecs.Add(prestec);
_notificador.Notificar($"Prestec de '{prestec.Llibre.Titol}' registrat.");
}
}
BibliotecaDbContext es registra amb AddDbContext (equivalent a AddScoped) perquè no està
pensat per compartir-se entre peticions concurrents (lliçó d'ASP.NET Core, Mòdul 7). Si
IRepositoriBiblioteca (que embolcalla aquest DbContext) o Biblioteca (que depèn
d'IRepositoriBiblioteca) es registressin com a AddSingleton, la primera petició HTTP
deixaria "atrapat" dins d'aquest singleton un BibliotecaDbContext que s'hauria d'haver
descartat en acabar aquesta petició, i les peticions següents reutilitzarien un context
obsolet o ja alliberat, provocant errors en temps d'execució.
Conclusió
En aquesta lliçó has entès el problema de fons que resol la inversió de control (acoblament fort
i dificultat per provar), comparat la injecció per constructor —la recomanada per defecte— amb la
injecció per propietat i per mètode, extret una interfície IRepositoriBiblioteca que unifica
les quatre formes de persistència del Mòdul 5 darrere d'un contracte comú, i injectat aquesta
interfície a Biblioteca per desacoblar-la de qualsevol mecanisme concret. També has entès en
profunditat, per fi, la diferència entre AddSingleton, AddScoped i AddTransient al
contenidor de serveis d'ASP.NET Core, que només s'havia esmentat de passada al Mòdul 7.
Tot aquest treball de desacoblar Biblioteca de les seves dependències concretes té un benefici
que encara no has explotat: ara és possible substituir IRepositoriBiblioteca per una versió de
prova que no toqui cap fitxer ni base de dades real. Aquesta és, precisament, la porta d'entrada
a la lliçó següent: proves unitàries, on IRepositoriBiblioteca se substituirà per un doble
de prova (mock) per verificar la lògica de Biblioteca de forma ràpida, aïllada i repetible.
Curs de Programació en C#
Mòdul 1: Introducció al C#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
