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
- Qué es un patrón de diseño y por qué importa
- Patrones creacionales: Singleton y Factory Method
- Patrones estructurales: Adapter y Decorator
- Patrones de comportamiento: Strategy y Observer
- Tabla resumen: cuándo usar cada patrón
- Ejemplo: Factory Method para crear
MaterialBibliotecario - Ejemplo: Strategy para políticas de cálculo de sanciones
- 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.
- 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 objetoEl 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 LibroEl 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.
- 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 BorgesMaterialConEtiquetaNuevo 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.
- 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 2Sin 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.
- 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) |
- Ejemplo: Factory Method para crear
MaterialBibliotecario
MaterialBibliotecarioRetomando 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 302Si 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.
- 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/elseque 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
-
Añade a
FabricaMaterial.Crear(apartado 6) un tercer caso para un tipo"audiolibro"que cree una claseAudioLibro : MaterialBibliotecario(puedes asumir que ya existe, con un constructorAudioLibro(string titulo, string autor, int duracionMinutos)), interpretandodatoAdicionalcomo la duración en minutos. -
Implementa una tercera política
IPoliticaSancionllamadaSinSancion, que siempre devuelva0msin 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#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
