La lliçó anterior va polir l'estil del codi de BiblioTech: noms, comentaris, documentació, nul·labilitat. Aquesta lliçó fa un pas més enllà de l'estil, cap al disseny: com organitzar les relacions entre classes per resoldre problemes que apareixen una i altra vegada en qualsevol projecte orientat a objectes. Un patró de disseny és, precisament, una solució ja provada a un d'aquests problemes recurrents —no codi que es copia literalment, sinó una manera de pensar l'estructura de les classes implicades—. Veuràs tres famílies clàssiques de patrons i n'aplicaràs dos directament sobre el domini de BiblioTech: Factory Method per crear materials bibliotecaris, i Strategy per calcular sancions per retard. Descobriràs, a més, que un dels patrons de comportament més coneguts —Observer— ja porta diversos mòduls aplicat a BiblioTech, sense que se li hagués posat nom fins ara.
Contingut
- Què és un patró de disseny i per què importa
- Patrons creacionals: Singleton i Factory Method
- Patrons estructurals: Adapter i Decorator
- Patrons de comportament: Strategy i Observer
- Taula resum: quan fer servir cada patró
- Exemple: Factory Method per crear
MaterialBibliotecari - Exemple: Strategy per a polítiques de càlcul de sancions
- Què és un patró de disseny i per què importa
Un patró de disseny no és una biblioteca que s'instal·la ni una sintaxi nova de C#: és una plantilla conceptual, documentada i amb nom propi, per resoldre un problema de disseny que es repeteix en projectes molt diferents entre si. La idea es va popularitzar amb el llibre Design Patterns (1994, l'anomenat "Gang of Four"), que va catalogar vint-i-tres patrons agrupats en tres famílies:
| Família | Què resol | Exemples d'aquesta lliçó |
|---|---|---|
| Creacionals | Com es creen els objectes, ocultant o flexibilitzant el procés de construcció | Singleton, Factory Method |
| Estructurals | Com es combinen classes i objectes per formar estructures més grans | Adapter, Decorator |
| De comportament | Com es comuniquen i reparteixen responsabilitats els objectes entre si | Strategy, Observer |
Conèixer aquests patrons importa per dues raons pràctiques: primer, donen vocabulari comú —dir "això és un Observer" comunica de cop tota una estructura a un altre desenvolupador, sense haver-la d'explicar des de zero—; segon, eviten reinventar pitjor una solució que ja està provada i té avantatges i inconvenients ben coneguts. No es tracta de forçar un patró a cada classe que s'escriu —un patró mal aplicat on no cal afegeix complexitat innecessària—, sinó de reconèixer quan el problema que tens al davant ja té una solució de disseny amb nom.
- Patrons creacionals: Singleton i Factory Method
Singleton
Singleton garanteix que una classe té una única instància a tota l'aplicació, i ofereix un punt d'accés global a ella:
class ConfiguracioBiblioTech
{
private static ConfiguracioBiblioTech? _instancia;
public string CadenaConnexio { get; }
private ConfiguracioBiblioTech()
{
CadenaConnexio = "Data Source=bibliotech.db";
}
public static ConfiguracioBiblioTech Instancia
{
get
{
_instancia ??= new ConfiguracioBiblioTech(); // crea la instancia nomes la primera vegada
return _instancia;
}
}
}Console.WriteLine(ConfiguracioBiblioTech.Instancia.CadenaConnexio);
// Qualsevol part del programa que accedeixi a "Instancia" obte sempre el mateix objecteEl constructor private impedeix crear instàncies amb new des de fora de la classe; l'única
via d'accés és la propietat estàtica Instancia, que crea l'objecte la primera vegada que es
demana (??=, l'operador d'assignació condicional nul·la) i retorna sempre aquesta mateixa
referència després. Singleton és útil per a configuració global compartida, però convé fer-lo
servir amb moderació: abusar-ne dificulta les proves unitàries (Lliçó 4 d'aquest mòdul), perquè
introdueix un estat global ocult que és difícil de substituir per una versió de prova.
Factory Method
Factory Method encapsula la lògica de quina classe concreta crear dins d'un mètode
dedicat, en comptes d'escampar crides a new TipusConcret(...) per tot el programa. Qui demana
un objecte no necessita saber la classe exacta que rebrà, només el tipus base o la interfície
comuna:
static class FabricaMaterial
{
public static MaterialBibliotecari Crear(string tipus, string titol, string autor)
{
return tipus switch
{
"llibre" => new Llibre(titol, autor, isbn: "SENSE-ISBN"),
"revista" => new Revista(titol, autor, numeroEdicio: 1),
_ => throw new ArgumentException($"Tipus de material desconegut: '{tipus}'")
};
}
}MaterialBibliotecari material = FabricaMaterial.Crear("llibre", "Rayuela", "Julio Cortazar");
Console.WriteLine(material.Descriure()); // el codi que crida no sap (ni necessita saber) que es un LlibreEl codi que crida FabricaMaterial.Crear no escriu new Llibre(...) ni new Revista(...)
directament: delega aquesta decisió a la fàbrica, a partir d'un simple string. Això centralitza
en un únic lloc la lògica de "quina classe concreta correspon a cada tipus", en comptes de
repetir-la —amb el risc que es desincronitzi— a cada punt del programa que necessiti crear un
material. L'apartat 6 amplia aquest exemple amb més detall.
- Patrons estructurals: Adapter i Decorator
Adapter
Adapter adapta la interfície d'una classe existent (sovint, una que no es pot modificar) perquè encaixi amb la interfície que la resta del codi espera. Imagina un servei extern de catàleg de biblioteques que exposa les seves dades amb noms de camp diferents als de BiblioTech:
class CatalegExternLlegat
{
public string GetBookTitle() => "Cien Anios de Soledad";
public string GetBookAuthor() => "Gabriel Garcia Marquez";
}
class AdaptadorCatalegExtern : MaterialBibliotecari
{
private readonly CatalegExternLlegat _origen;
public AdaptadorCatalegExtern(CatalegExternLlegat origen)
: base(origen.GetBookTitle(), origen.GetBookAuthor())
{
_origen = origen;
}
public override string Descriure() => $"(Extern) {Titol}, de {Autor}";
}AdaptadorCatalegExtern hereta de MaterialBibliotecari i, per dins, tradueix les crides a
GetBookTitle()/GetBookAuthor() de CatalegExternLlegat cap al constructor que
MaterialBibliotecari ja espera. La resta de BiblioTech pot tractar un material que ve d'aquest
servei extern exactament igual que un Llibre o una Revista propis, sense conèixer mai la
interfície original de CatalegExternLlegat.
Decorator
Decorator afegeix responsabilitats a un objecte embolcallant-lo, sense modificar-ne la classe ni fer servir herència per a cada combinació possible de comportaments afegits:
abstract class DecoradorMaterial : MaterialBibliotecari
{
protected readonly MaterialBibliotecari Material;
protected DecoradorMaterial(MaterialBibliotecari material)
: base(material.Titol, material.Autor)
{
Material = material;
}
}
class MaterialAmbEtiquetaNova : DecoradorMaterial
{
public MaterialAmbEtiquetaNova(MaterialBibliotecari material) : base(material) { }
public override string Descriure() => $"[NOU] {Material.Descriure()}";
}MaterialBibliotecari llibre = new Llibre("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
MaterialBibliotecari llibreDestacat = new MaterialAmbEtiquetaNova(llibre);
Console.WriteLine(llibreDestacat.Descriure()); // [NOU] Llibre: Ficciones, de Jorge Luis BorgesMaterialAmbEtiquetaNova embolcalla qualsevol MaterialBibliotecari i li afegeix el prefix
[NOU] a la seva descripció, sense tocar ni Llibre ni Revista ni necessitar una classe
LlibreNou/RevistaNova separada per a cada combinació. Es podrien encadenar diversos
decoradors (per exemple, un que afegeixi [DESTACAT] sobre l'anterior) per combinar
comportaments de forma flexible.
- Patrons de comportament: Strategy i Observer
Strategy
Strategy encapsula un algorisme intercanviable darrere d'una interfície comuna, de manera que
l'algorisme concret es pot canviar en temps d'execució sense tocar el codi que l'usa. L'apartat 7
ho desenvolupa en profunditat amb les polítiques de sancions de Prestec.
Observer: PrestecRegistrat ja és un Observer
Observer defineix una dependència d'un a molts entre objectes: quan un objecte (el
subjecte) canvia d'estat, tots els seus observadors són notificats automàticament, sense
que el subjecte necessiti conèixer-los per endavant. Repassa l'esdeveniment PrestecRegistrat de
Biblioteca, introduït a la lliçó de Delegats i Esdeveniments del Mòdul 4:
class Biblioteca
{
public event Action<Prestec> PrestecRegistrat; // el "subjecte" observat
public void RegistrarPrestec(Prestec prestec)
{
Prestecs.Add(prestec);
PrestecRegistrat?.Invoke(prestec); // notifica a tots els "observadors" subscrits
}
public List<Prestec> Prestecs { get; } = new List<Prestec>();
}biblioteca.PrestecRegistrat += prestec =>
Console.WriteLine($"[Log] Prestec de '{prestec.Llibre.Titol}' registrat."); // observador 1
biblioteca.PrestecRegistrat += prestec =>
Console.WriteLine($"S'ha prestat '{prestec.Llibre.Titol}' a {prestec.Soci.Nom}"); // observador 2Sense que ningú li hagués posat nom fins ara, aquesta estructura és el patró Observer:
Biblioteca és el subjecte, cada mètode subscrit amb += és un observador, i event és el
mecanisme concret que C# ofereix per implementar aquest patró de forma segura (recorda la lliçó
de Delegats i Esdeveniments: event impedeix invocar o reemplaçar la llista de subscriptors des
de fora de la classe). És habitual trobar-se que ja s'ha fet servir un patró de disseny sense
saber-ho —reconèixer-ho ara, amb nom, ajuda a raonar-hi amb el vocabulari compartit de la
comunitat C#, i a reconèixer la mateixa estructura la propera vegada que aparegui.
- Taula resum: quan fer servir cada patró
| Patró | Família | Resol | Ja vist/usat a BiblioTech |
|---|---|---|---|
| Singleton | Creacional | Garantir una única instància global | ConfiguracioBiblioTech (apartat 2) |
| Factory Method | Creacional | Centralitzar la lògica de quina classe concreta crear | FabricaMaterial (apartats 2 i 6) |
| Adapter | Estructural | Adaptar una interfície existent incompatible a l'esperada | AdaptadorCatalegExtern (apartat 3) |
| Decorator | Estructural | Afegir responsabilitats a un objecte sense herència ni modificar-lo | MaterialAmbEtiquetaNova (apartat 3) |
| Strategy | Comportament | Intercanviar un algorisme en temps d'execució | Polítiques de sancions de Prestec (apartat 7) |
| Observer | Comportament | Notificar diversos interessats d'un canvi d'estat | Biblioteca.PrestecRegistrat (Mòdul 4) |
- Exemple: Factory Method per crear
MaterialBibliotecari
MaterialBibliotecariRetomant FabricaMaterial de l'apartat 2, una versió més completa que reflecteix com es faria
servir a BiblioTech en importar materials des d'una font externa (per exemple, un fitxer CSV o
una resposta JSON, Mòdul 5) on el tipus de material arriba com a text:
static class FabricaMaterial
{
/// <summary>
/// Crea el material bibliotecari adequat a partir d'un tipus textual.
/// </summary>
/// <param name="tipus">"llibre" o "revista", sense distingir majuscules/minuscules.</param>
/// <param name="titol">Titol del material.</param>
/// <param name="autor">Autor del material.</param>
/// <param name="dadaAddicional">L'ISBN si es un llibre, o el numero d'edicio (com a text) si es una revista.</param>
/// <returns>Una instancia de <see cref="Llibre"/> o <see cref="Revista"/> segons el tipus indicat.</returns>
public static MaterialBibliotecari Crear(string tipus, string titol, string autor, string dadaAddicional)
{
return tipus.ToLower() switch
{
"llibre" => new Llibre(titol, autor, isbn: dadaAddicional),
"revista" => new Revista(titol, autor, numeroEdicio: int.Parse(dadaAddicional)),
_ => throw new ArgumentException($"Tipus de material desconegut: '{tipus}'")
};
}
}List<(string Tipus, string Titol, string Autor, string Dada)> materialsImportats = new()
{
("llibre", "Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
("revista", "National Geographic", "Diversos autors", "302")
};
foreach (var (tipus, titol, autor, dada) in materialsImportats)
{
MaterialBibliotecari material = FabricaMaterial.Crear(tipus, titol, autor, dada);
Console.WriteLine(material.Descriure());
}
// Llibre: Rayuela, de Julio Cortazar (ISBN 978-84-376-0495-4)
// Revista: National Geographic, edicio numero 302Si BiblioTech afegís en el futur un tercer tipus de material (per exemple, AudioLlibre), l'únic
canvi necessari seria afegir un nou cas al switch de FabricaMaterial.Crear; cap altre punt del
programa que ja cridi la fàbrica necessitaria modificar-se.
- Exemple: Strategy per a polítiques de càlcul de sancions
BiblioTech necessita calcular una sanció quan un préstec es retorna amb retard. En comptes
d'escriure aquesta lògica directament dins de Prestec (cosa que obligaria a modificar la classe
cada vegada que canviés la política de sancions), Strategy l'extreu a una interfície
intercanviable:
interface IPoliticaSancio
{
decimal CalcularSancio(int diesRetard);
}
class SancioFixa : IPoliticaSancio
{
public decimal CalcularSancio(int diesRetard) => diesRetard > 0 ? 2.00m : 0m;
}
class SancioProgressiva : IPoliticaSancio
{
public decimal CalcularSancio(int diesRetard) => diesRetard > 0 ? diesRetard * 0.50m : 0m;
}SancioFixa cobra sempre el mateix si hi ha retard, sense importar quants dies; SancioProgressiva
cobra més com més dies de retard hi hagi. Totes dues implementen la mateixa interfície
IPoliticaSancio, així que són intercanviables entre si. Prestec rep la política a aplicar
sense conèixer els detalls de cap de les dues implementacions:
class Prestec
{
public Llibre Llibre { get; }
public Soci Soci { get; }
public DateTime DataPrestec { get; }
public DateTime? DataDevolucio { get; private set; }
public Prestec(Llibre llibre, Soci soci)
{
Llibre = llibre;
Soci = soci;
DataPrestec = DateTime.Now;
}
public void RegistrarDevolucio()
{
DataDevolucio = DateTime.Now;
}
public decimal CalcularSancio(IPoliticaSancio politica, int diesPermesos = 14)
{
if (DataDevolucio is null)
{
return 0m; // encara no s'ha retornat, no hi ha sancio a calcular de moment
}
int diesTranscorreguts = (DataDevolucio.Value - DataPrestec).Days;
int diesRetard = Math.Max(0, diesTranscorreguts - diesPermesos);
return politica.CalcularSancio(diesRetard);
}
}Prestec prestec1 = new Prestec(llibre1, soci1);
prestec1.RegistrarDevolucio();
decimal sancioFixa = prestec1.CalcularSancio(new SancioFixa());
decimal sancioProgressiva = prestec1.CalcularSancio(new SancioProgressiva());
Console.WriteLine($"Sancio fixa: {sancioFixa:C}, sancio progressiva: {sancioProgressiva:C}");CalcularSancio rep la política com a paràmetre (IPoliticaSancio politica): Prestec no sap,
ni necessita saber, si s'està aplicant SancioFixa, SancioProgressiva, o una tercera política
que s'afegeixi en el futur (per exemple, una que eximeixi de sanció els socis amb una antiguitat
determinada). Canviar de política —fins i tot en temps d'execució, decidint quina fer servir
segons alguna condició— no requereix cap modificació a Prestec.
Errors Comuns i Consells
- Forçar un patró on no cal: aplicar Strategy per a un
if/elseque mai canviarà, o Decorator per a una única variació que mai es combina amb una altra, afegeix complexitat sense cap benefici real. Els patrons resolen problemes de variació i creixement; si no existeix aquest problema, el codi directe i senzill és la millor opció. - Confondre Adapter amb Decorator: tots dos "embolcallen" un objecte, però amb propòsits diferents: Adapter tradueix una interfície incompatible a l'esperada; Decorator afegeix comportament a una interfície que ja era compatible des del principi.
- Abusar de Singleton per a "qualsevol cosa global": Singleton dificulta les proves unitàries (Lliçó 4) perquè introdueix un estat compartit difícil de substituir per una versió de prova. La Lliçó 3 d'aquest mòdul (Injecció de Dependències) ofereix, per a la majoria de casos, una alternativa més flexible i més fàcil de provar.
- Consell: no memoritzis els vint-i-tres patrons del catàleg original; n'hi ha prou amb reconèixer, quan apareix un problema conegut (crear objectes segons un tipus variable, notificar diversos interessats, intercanviar un algorisme), quin patró ja resol exactament aquest problema.
Exercicis
-
Afegeix a
FabricaMaterial.Crear(apartat 6) un tercer cas per a un tipus"audiollibre"que creï una classeAudioLlibre : MaterialBibliotecari(pots assumir que ja existeix, amb un constructorAudioLlibre(string titol, string autor, int duracioMinuts)), interpretantdadaAddicionalcom la durada en minuts. -
Implementa una tercera política
IPoliticaSancioanomenadaSenseSancio, que sempre retorni0msense importar els dies de retard, i explica en una frase en quin escenari de BiblioTech tindria sentit fer-la servir.
Solucions
public static MaterialBibliotecari Crear(string tipus, string titol, string autor, string dadaAddicional)
{
return tipus.ToLower() switch
{
"llibre" => new Llibre(titol, autor, isbn: dadaAddicional),
"revista" => new Revista(titol, autor, numeroEdicio: int.Parse(dadaAddicional)),
"audiollibre" => new AudioLlibre(titol, autor, duracioMinuts: int.Parse(dadaAddicional)),
_ => throw new ArgumentException($"Tipus de material desconegut: '{tipus}'")
};
}
class SenseSancio : IPoliticaSancio
{
public decimal CalcularSancio(int diesRetard) => 0m;
}
Tindria sentit, per exemple, per a socis amb algun tipus de bonificació (personal de la
mateixa biblioteca, o una promoció temporal), on es vol reutilitzar tota la lògica de
Prestec.CalcularSancio sense aplicar cap penalització econòmica real.
Conclusió
En aquesta lliçó has conegut què és un patró de disseny i per què dona vocabulari comú i
solucions ja provades, has vist Singleton i Factory Method (creacionals), Adapter i Decorator
(estructurals), i Strategy i Observer (de comportament) —descobrint que aquest últim ja portava
aplicat a Biblioteca.PrestecRegistrat des del Mòdul 4—, i has aplicat Factory Method per
centralitzar la creació de materials i Strategy per fer intercanviables les polítiques de sanció
de Prestec. La lliçó següent retoma en profunditat un mecanisme que ja va aparèixer esmentat de
passada a diverses interfícies del Mòdul 7 (ASP.NET Core, Blazor, MAUI): la injecció de
dependències, que resulta ser, en el fons, una forma sistemàtica d'aplicar el mateix principi
que fa possible Strategy —programar contra interfícies, no contra implementacions concretes— a
tot el projecte BiblioTech.
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
