La lección anterior pulió el estilo del código de BiblioTech: nombres, comentarios, documentación, nulabilidad. Esta lección da un paso más allá del estilo, hacia el diseño: cómo organizar las relaciones entre clases para resolver problemas que aparecen una y otra vez en cualquier proyecto orientado a objetos. Un patrón de diseño es, precisamente, una solución ya probada a uno de esos problemas recurrentes —no código que se copia literalmente, sino una forma de pensar la estructura de las clases involucradas—. Verás tres familias clásicas de patrones y aplicarás dos de ellos directamente sobre el dominio de BiblioTech: Factory Method para crear materiales bibliotecarios, y Strategy para calcular sanciones por retraso. Descubrirás, además, que uno de los patrones de comportamiento más conocidos —Observer— ya lleva varios módulos aplicado en BiblioTech, sin que se le hubiera puesto nombre hasta ahora.

Contenido

  1. Qué es un patrón de diseño y por qué importa
  2. Patrones creacionales: Singleton y Factory Method
  3. Patrones estructurales: Adapter y Decorator
  4. Patrones de comportamiento: Strategy y Observer
  5. Tabla resumen: cuándo usar cada patrón
  6. Ejemplo: Factory Method para crear MaterialBibliotecario
  7. Ejemplo: Strategy para políticas de cálculo de sanciones

  1. Qué es un patrón de diseño y por qué importa

Un patrón de diseño no es una biblioteca que se instala ni una sintaxis nueva de C#: es una plantilla conceptual, documentada y con nombre propio, para resolver un problema de diseño que se repite en proyectos muy distintos entre sí. La idea se popularizó con el libro Design Patterns (1994, el llamado "Gang of Four"), que catalogó veintitrés patrones agrupados en tres familias:

Familia Qué resuelve Ejemplos de esta lección
Creacionales Cómo se crean los objetos, ocultando o flexibilizando el proceso de construcción Singleton, Factory Method
Estructurales Cómo se combinan clases y objetos para formar estructuras más grandes Adapter, Decorator
De comportamiento Cómo se comunican y reparten responsabilidades los objetos entre sí Strategy, Observer

Conocer estos patrones importa por dos razones prácticas: primero, dan vocabulario común —decir "esto es un Observer" comunica de golpe toda una estructura a otro desarrollador, sin tener que explicarla desde cero—; segundo, evitan reinventar peor una solución que ya está probada y tiene ventajas e inconvenientes bien conocidos. No se trata de forzar un patrón en cada clase que se escribe —un patrón mal aplicado donde no hace falta añade complejidad innecesaria—, sino de reconocer cuándo el problema que tienes delante ya tiene una solución de diseño con nombre.

  1. Patrones creacionales: Singleton y Factory Method

Singleton

Singleton garantiza que una clase tiene una única instancia en toda la aplicación, y ofrece un punto de acceso global a ella:

class ConfiguracionBiblioTech
{
    private static ConfiguracionBiblioTech? _instancia;

    public string CadenaConexion { get; }

    private ConfiguracionBiblioTech()
    {
        CadenaConexion = "Data Source=bibliotech.db";
    }

    public static ConfiguracionBiblioTech Instancia
    {
        get
        {
            _instancia ??= new ConfiguracionBiblioTech(); // crea la instancia solo la primera vez
            return _instancia;
        }
    }
}
Console.WriteLine(ConfiguracionBiblioTech.Instancia.CadenaConexion);
// Cualquier parte del programa que acceda a "Instancia" obtiene siempre el mismo objeto

El constructor private impide crear instancias con new desde fuera de la clase; la única vía de acceso es la propiedad estática Instancia, que crea el objeto la primera vez que se pide (??=, el operador de asignación condicional nula) y devuelve siempre esa misma referencia después. Singleton es útil para configuración global compartida, pero conviene usarlo con moderación: abusar de él dificulta las pruebas unitarias (Lección 4 de este módulo), porque introduce un estado global oculto que es difícil de sustituir por una versión de prueba.

Factory Method

Factory Method encapsula la lógica de qué clase concreta crear dentro de un método dedicado, en vez de esparcir llamadas a new TipoConcreto(...) por todo el programa. Quien pide un objeto no necesita saber la clase exacta que recibirá, solo el tipo base o la interfaz común:

static class FabricaMaterial
{
    public static MaterialBibliotecario Crear(string tipo, string titulo, string autor)
    {
        return tipo switch
        {
            "libro" => new Libro(titulo, autor, isbn: "SIN-ISBN"),
            "revista" => new Revista(titulo, autor, numeroEdicion: 1),
            _ => throw new ArgumentException($"Tipo de material desconocido: '{tipo}'")
        };
    }
}
MaterialBibliotecario material = FabricaMaterial.Crear("libro", "Rayuela", "Julio Cortazar");
Console.WriteLine(material.Describir()); // el codigo que llama no sabe (ni necesita saber) que es un Libro

El código que llama a FabricaMaterial.Crear no escribe new Libro(...) ni new Revista(...) directamente: delega esa decisión en la fábrica, a partir de un simple string. Esto centraliza en un único lugar la lógica de "qué clase concreta corresponde a cada tipo", en vez de repetirla —y con el riesgo de que se desincronice— en cada punto del programa que necesite crear un material. El apartado 6 amplía este ejemplo con más detalle.

  1. Patrones estructurales: Adapter y Decorator

Adapter

Adapter adapta la interfaz de una clase existente (a menudo, una que no se puede modificar) para que encaje con la interfaz que el resto del código espera. Imagina un servicio externo de catálogo de bibliotecas que expone sus datos con nombres de campo distintos a los de BiblioTech:

class CatalogoExternoLegado
{
    public string GetBookTitle() => "Cien Anios de Soledad";
    public string GetBookAuthor() => "Gabriel Garcia Marquez";
}

class AdaptadorCatalogoExterno : MaterialBibliotecario
{
    private readonly CatalogoExternoLegado _origen;

    public AdaptadorCatalogoExterno(CatalogoExternoLegado origen)
        : base(origen.GetBookTitle(), origen.GetBookAuthor())
    {
        _origen = origen;
    }

    public override string Describir() => $"(Externo) {Titulo}, de {Autor}";
}

AdaptadorCatalogoExterno hereda de MaterialBibliotecario y, por dentro, traduce las llamadas a GetBookTitle()/GetBookAuthor() de CatalogoExternoLegado hacia el constructor que MaterialBibliotecario ya espera. El resto de BiblioTech puede tratar un material que viene de ese servicio externo exactamente igual que un Libro o una Revista propios, sin conocer nunca la interfaz original de CatalogoExternoLegado.

Decorator

Decorator añade responsabilidades a un objeto envolviéndolo, sin modificar su clase ni usar herencia para cada combinación posible de comportamientos añadidos:

abstract class DecoradorMaterial : MaterialBibliotecario
{
    protected readonly MaterialBibliotecario Material;

    protected DecoradorMaterial(MaterialBibliotecario material)
        : base(material.Titulo, material.Autor)
    {
        Material = material;
    }
}

class MaterialConEtiquetaNuevo : DecoradorMaterial
{
    public MaterialConEtiquetaNuevo(MaterialBibliotecario material) : base(material) { }

    public override string Describir() => $"[NUEVO] {Material.Describir()}";
}
MaterialBibliotecario libro = new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
MaterialBibliotecario libroDestacado = new MaterialConEtiquetaNuevo(libro);

Console.WriteLine(libroDestacado.Describir()); // [NUEVO] Libro: Ficciones, de Jorge Luis Borges

MaterialConEtiquetaNuevo envuelve cualquier MaterialBibliotecario y le añade el prefijo [NUEVO] a su descripción, sin tocar ni Libro ni Revista ni necesitar una clase LibroNuevo/RevistaNueva separada para cada combinación. Se podrían encadenar varios decoradores (por ejemplo, uno que añada [DESTACADO] sobre el anterior) para combinar comportamientos de forma flexible.

  1. Patrones de comportamiento: Strategy y Observer

Strategy

Strategy encapsula un algoritmo intercambiable detrás de una interfaz común, de modo que el algoritmo concreto se puede cambiar en tiempo de ejecución sin tocar el código que lo usa. El apartado 7 lo desarrolla en profundidad con las políticas de sanciones de Prestamo.

Observer: PrestamoRegistrado ya es un Observer

Observer define una dependencia uno-a-muchos entre objetos: cuando un objeto (el sujeto) cambia de estado, todos sus observadores son notificados automáticamente, sin que el sujeto necesite conocerlos de antemano. Repasa el evento PrestamoRegistrado de Biblioteca, introducido en la lección de Delegados y Eventos del Módulo 4:

class Biblioteca
{
    public event Action<Prestamo> PrestamoRegistrado; // el "sujeto" observado

    public void RegistrarPrestamo(Prestamo prestamo)
    {
        Prestamos.Add(prestamo);
        PrestamoRegistrado?.Invoke(prestamo); // notifica a todos los "observadores" suscritos
    }

    public List<Prestamo> Prestamos { get; } = new List<Prestamo>();
}
biblioteca.PrestamoRegistrado += prestamo =>
    Console.WriteLine($"[Log] Prestamo de '{prestamo.Libro.Titulo}' registrado.");   // observador 1

biblioteca.PrestamoRegistrado += prestamo =>
    Console.WriteLine($"Se ha prestado '{prestamo.Libro.Titulo}' a {prestamo.Socio.Nombre}"); // observador 2

Sin que nadie le hubiera puesto nombre hasta ahora, esta estructura es el patrón Observer: Biblioteca es el sujeto, cada método suscrito con += es un observador, y event es el mecanismo concreto que C# ofrece para implementar este patrón de forma segura (recuerda la lección de Delegados y Eventos: event impide invocar o reemplazar la lista de suscriptores desde fuera de la clase). Es habitual encontrarse con que ya se ha usado un patrón de diseño sin saberlo —reconocerlo ahora, con nombre, ayuda a razonar sobre él con el vocabulario compartido de la comunidad C#, y a reconocer la misma estructura la próxima vez que aparezca.

  1. Tabla resumen: cuándo usar cada patrón

Patrón Familia Resuelve Ya visto/usado en BiblioTech
Singleton Creacional Garantizar una única instancia global ConfiguracionBiblioTech (apartado 2)
Factory Method Creacional Centralizar la lógica de qué clase concreta crear FabricaMaterial (apartados 2 y 6)
Adapter Estructural Adaptar una interfaz existente incompatible a la esperada AdaptadorCatalogoExterno (apartado 3)
Decorator Estructural Añadir responsabilidades a un objeto sin herencia ni modificarlo MaterialConEtiquetaNuevo (apartado 3)
Strategy Comportamiento Intercambiar un algoritmo en tiempo de ejecución Políticas de sanciones de Prestamo (apartado 7)
Observer Comportamiento Notificar a varios interesados de un cambio de estado Biblioteca.PrestamoRegistrado (Módulo 4)

  1. Ejemplo: Factory Method para crear MaterialBibliotecario

Retomando FabricaMaterial del apartado 2, una versión más completa que refleja cómo se usaría en BiblioTech al importar materiales desde una fuente externa (por ejemplo, un fichero CSV o una respuesta JSON, Módulo 5) donde el tipo de material llega como texto:

static class FabricaMaterial
{
    /// <summary>
    /// Crea el material bibliotecario adecuado a partir de un tipo textual.
    /// </summary>
    /// <param name="tipo">"libro" o "revista", sin distinguir mayusculas/minusculas.</param>
    /// <param name="titulo">Titulo del material.</param>
    /// <param name="autor">Autor del material.</param>
    /// <param name="datoAdicional">El ISBN si es un libro, o el numero de edicion (como texto) si es una revista.</param>
    /// <returns>Una instancia de <see cref="Libro"/> o <see cref="Revista"/> segun el tipo indicado.</returns>
    public static MaterialBibliotecario Crear(string tipo, string titulo, string autor, string datoAdicional)
    {
        return tipo.ToLower() switch
        {
            "libro" => new Libro(titulo, autor, isbn: datoAdicional),
            "revista" => new Revista(titulo, autor, numeroEdicion: int.Parse(datoAdicional)),
            _ => throw new ArgumentException($"Tipo de material desconocido: '{tipo}'")
        };
    }
}
List<(string Tipo, string Titulo, string Autor, string Dato)> materialesImportados = new()
{
    ("libro", "Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
    ("revista", "National Geographic", "Varios autores", "302")
};

foreach (var (tipo, titulo, autor, dato) in materialesImportados)
{
    MaterialBibliotecario material = FabricaMaterial.Crear(tipo, titulo, autor, dato);
    Console.WriteLine(material.Describir());
}
// Libro: Rayuela, de Julio Cortazar (ISBN 978-84-376-0495-4)
// Revista: National Geographic, edicion numero 302

Si BiblioTech añadiera en el futuro un tercer tipo de material (por ejemplo, AudioLibro), el único cambio necesario sería añadir un nuevo caso al switch de FabricaMaterial.Crear; ningún otro punto del programa que ya llame a la fábrica necesitaría modificarse.

  1. Ejemplo: Strategy para políticas de cálculo de sanciones

BiblioTech necesita calcular una sanción cuando un préstamo se devuelve con retraso. En vez de escribir esa lógica directamente dentro de Prestamo (lo que obligaría a modificar la clase cada vez que cambiara la política de sanciones), Strategy la extrae a una interfaz intercambiable:

interface IPoliticaSancion
{
    decimal CalcularSancion(int diasRetraso);
}

class SancionFija : IPoliticaSancion
{
    public decimal CalcularSancion(int diasRetraso) => diasRetraso > 0 ? 2.00m : 0m;
}

class SancionProgresiva : IPoliticaSancion
{
    public decimal CalcularSancion(int diasRetraso) => diasRetraso > 0 ? diasRetraso * 0.50m : 0m;
}

SancionFija cobra siempre lo mismo si hay retraso, sin importar cuántos días; SancionProgresiva cobra más cuantos más días de retraso haya. Ambas implementan la misma interfaz IPoliticaSancion, así que son intercambiables entre sí. Prestamo recibe la política a aplicar sin conocer los detalles de ninguna de las dos implementaciones:

class Prestamo
{
    public Libro Libro { get; }
    public Socio Socio { get; }
    public DateTime FechaPrestamo { get; }
    public DateTime? FechaDevolucion { get; private set; }

    public Prestamo(Libro libro, Socio socio)
    {
        Libro = libro;
        Socio = socio;
        FechaPrestamo = DateTime.Now;
    }

    public void RegistrarDevolucion()
    {
        FechaDevolucion = DateTime.Now;
    }

    public decimal CalcularSancion(IPoliticaSancion politica, int diasPermitidos = 14)
    {
        if (FechaDevolucion is null)
        {
            return 0m; // aun no se ha devuelto, no hay sancion que calcular todavia
        }

        int diasTranscurridos = (FechaDevolucion.Value - FechaPrestamo).Days;
        int diasRetraso = Math.Max(0, diasTranscurridos - diasPermitidos);

        return politica.CalcularSancion(diasRetraso);
    }
}
Prestamo prestamo1 = new Prestamo(libro1, socio1);
prestamo1.RegistrarDevolucion();

decimal sancionFija = prestamo1.CalcularSancion(new SancionFija());
decimal sancionProgresiva = prestamo1.CalcularSancion(new SancionProgresiva());

Console.WriteLine($"Sancion fija: {sancionFija:C}, sancion progresiva: {sancionProgresiva:C}");

CalcularSancion recibe la política como parámetro (IPoliticaSancion politica): Prestamo no sabe, ni necesita saber, si se está aplicando SancionFija, SancionProgresiva, o una tercera política que se añada en el futuro (por ejemplo, una que exima de sanción a socios con una antigüedad determinada). Cambiar de política —incluso en tiempo de ejecución, decidiendo cuál usar según alguna condición— no requiere ninguna modificación en Prestamo.

Errores Comunes y Consejos

  • Forzar un patrón donde no hace falta: aplicar Strategy para un if/else que nunca va a cambiar, o Decorator para una única variación que nunca se combina con otra, añade complejidad sin ningún beneficio real. Los patrones resuelven problemas de variación y crecimiento; si no existe ese problema, el código directo y simple es la mejor opción.
  • Confundir Adapter con Decorator: ambos "envuelven" un objeto, pero con propósitos distintos: Adapter traduce una interfaz incompatible a la esperada; Decorator añade comportamiento a una interfaz que ya era compatible desde el principio.
  • Abusar de Singleton para "cualquier cosa global": Singleton dificulta las pruebas unitarias (Lección 4) porque introduce un estado compartido difícil de sustituir por una versión de prueba. La Lección 3 de este módulo (Inyección de Dependencias) ofrece, para la mayoría de casos, una alternativa más flexible y más fácil de testear.
  • Consejo: no memorices los veintitrés patrones del catálogo original; basta con reconocer, cuando aparece un problema conocido (crear objetos según un tipo variable, notificar a varios interesados, intercambiar un algoritmo), qué patrón ya resuelve exactamente ese problema.

Ejercicios

  1. Añade a FabricaMaterial.Crear (apartado 6) un tercer caso para un tipo "audiolibro" que cree una clase AudioLibro : MaterialBibliotecario (puedes asumir que ya existe, con un constructor AudioLibro(string titulo, string autor, int duracionMinutos)), interpretando datoAdicional como la duración en minutos.

  2. Implementa una tercera política IPoliticaSancion llamada SinSancion, que siempre devuelva 0m sin importar los días de retraso, y explica en una frase en qué escenario de BiblioTech tendría sentido usarla.

Soluciones

public static MaterialBibliotecario Crear(string tipo, string titulo, string autor, string datoAdicional)
{
    return tipo.ToLower() switch
    {
        "libro" => new Libro(titulo, autor, isbn: datoAdicional),
        "revista" => new Revista(titulo, autor, numeroEdicion: int.Parse(datoAdicional)),
        "audiolibro" => new AudioLibro(titulo, autor, duracionMinutos: int.Parse(datoAdicional)),
        _ => throw new ArgumentException($"Tipo de material desconocido: '{tipo}'")
    };
}
class SinSancion : IPoliticaSancion
{
    public decimal CalcularSancion(int diasRetraso) => 0m;
}

Tendría sentido, por ejemplo, para socios con algún tipo de bonificación (personal de la propia biblioteca, o una promoción temporal), donde se quiere reutilizar toda la lógica de Prestamo.CalcularSancion sin aplicar ninguna penalización económica real.

Conclusión

En esta lección has conocido qué es un patrón de diseño y por qué da vocabulario común y soluciones ya probadas, has visto Singleton y Factory Method (creacionales), Adapter y Decorator (estructurales), y Strategy y Observer (de comportamiento) —descubriendo que este último ya llevaba aplicado en Biblioteca.PrestamoRegistrado desde el Módulo 4—, y has aplicado Factory Method para centralizar la creación de materiales y Strategy para hacer intercambiables las políticas de sanción de Prestamo. La siguiente lección retoma en profundidad un mecanismo que ya apareció mencionado de pasada en varias interfaces del Módulo 7 (ASP.NET Core, Blazor, MAUI): la inyección de dependencias, que resulta ser, en el fondo, una forma sistemática de aplicar el mismo principio que hace posible Strategy —programar contra interfaces, no contra implementaciones concretas— a todo el proyecto BiblioTech.

Curso de Programación en C#

Módulo 1: Introducción a C#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados