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

  1. Què és un patró de disseny i per què importa
  2. Patrons creacionals: Singleton i Factory Method
  3. Patrons estructurals: Adapter i Decorator
  4. Patrons de comportament: Strategy i Observer
  5. Taula resum: quan fer servir cada patró
  6. Exemple: Factory Method per crear MaterialBibliotecari
  7. Exemple: Strategy per a polítiques de càlcul de sancions

  1. 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.

  1. 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 objecte

El 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 Llibre

El 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.

  1. 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 Borges

MaterialAmbEtiquetaNova 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.

  1. 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 2

Sense 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.

  1. 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)

  1. Exemple: Factory Method per crear MaterialBibliotecari

Retomant 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 302

Si 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.

  1. 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/else que 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

  1. Afegeix a FabricaMaterial.Crear (apartat 6) un tercer cas per a un tipus "audiollibre" que creï una classe AudioLlibre : MaterialBibliotecari (pots assumir que ja existeix, amb un constructor AudioLlibre(string titol, string autor, int duracioMinuts)), interpretant dadaAddicional com la durada en minuts.

  2. Implementa una tercera política IPoliticaSancio anomenada SenseSancio, que sempre retorni 0m sense 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#

Mòdul 2: Estructures de Control

Mòdul 3: Programació Orientada a Objectes

Mòdul 4: Conceptes Avançats de C#

Mòdul 5: Treballant amb Dades

Mòdul 6: Temes Avançats

Mòdul 7: Construcció d'Aplicacions

Mòdul 8: Bones Pràctiques i Patrons de Disseny

Mòdul 9: Projecte Final

© Copyright 2026. Tots els drets reservats