Patrones de diseño en C# (Factory, Singleton, Repository, Observer)
Aprende patrones de diseño en C#, como Factory, Singleton y Repository, para desarrollar aplicaciones escalables y mantenibles.
Los Patrones de Diseño (Design Patterns) proporcionan soluciones estándar y comprobadas para problemas recurrentes en el desarrollo de software. Los patrones más utilizados en C# incluyen Factory, Singleton, Repository, Observer, Strategy, Adapter y Decorator. Este artículo explora los conceptos básicos de estos patrones, sus casos de uso y ejemplos en C#.
Patrón Factory
El patrón Factory centraliza la creación de objetos.
En lugar de usar new en todas partes, la responsabilidad de crear objetos se delega a una clase Factory.
public interface IProduct { void DoWork(); }
public class ProductA : IProduct
{
public void DoWork() => Console.WriteLine("Producto A");
}
public class ProductB : IProduct
{
public void DoWork() => Console.WriteLine("Producto B");
}
public static class ProductFactory
{
public static IProduct Create(string type) => type switch
{
"A" => new ProductA(),
"B" => new ProductB(),
_ => throw new ArgumentException("Tipo inválido")
};
}
Patrón Singleton
El patrón Singleton garantiza que solo se cree una única instancia de una clase. Se utiliza comúnmente para logging, caché u objetos de configuración.
public class Singleton
{
private static Singleton? _instance;
private static readonly object _lock = new();
private Singleton() { }
public static Singleton Instance
{
get
{
lock (_lock)
return _instance ??= new Singleton();
}
}
}
Patrón Repository
El patrón Repository abstrae la capa de acceso a datos. De esta forma, la capa de lógica de negocio se vuelve independiente de los detalles de la base de datos.
public class Product { public int Id; public string Name = ""; }
public interface IProductRepository
{
void Add(Product p);
Product? GetById(int id);
}
public class InMemoryProductRepository : IProductRepository
{
private readonly List<Product> _list = new();
public void Add(Product p) => _list.Add(p);
public Product? GetById(int id) => _list.FirstOrDefault(x => x.Id == id);
}
Patrón Observer
El patrón Observer establece una relación publicador–suscriptor. Cuando un objeto cambia, todos los suscriptores son notificados. En C#, esto se implementa con el mecanismo de eventos y delegados.
public class NewsAgency
{
public event EventHandler<string>? NewsPublished;
public void Publish(string news)
{
Console.WriteLine($"Noticia: {news}");
NewsPublished?.Invoke(this, news);
}
}
Patrón Strategy
El patrón Strategy encapsula algoritmos de manera que se pueden intercambiar fácilmente. La clase cliente no necesita saber qué algoritmo se está utilizando; la estrategia se proporciona externamente.
public interface IPaymentStrategy { void Pay(decimal amount); }
public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"{amount} USD pagado con tarjeta de crédito.");
}
public class PayPalPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"{amount} USD pagado vía PayPal.");
}
public class Checkout
{
private readonly IPaymentStrategy _strategy;
public Checkout(IPaymentStrategy strategy) => _strategy = strategy;
public void ProcessOrder(decimal total) => _strategy.Pay(total);
}
Patrón Adapter
El patrón Adapter permite que dos clases incompatibles trabajen juntas. A menudo se utiliza para integrar bibliotecas o APIs heredadas en sistemas nuevos.
// Sistema antiguo
public class LegacyPrinter { public void PrintText(string t) => Console.WriteLine(t); }
// Nueva interfaz
public interface IPrinter { void Print(string text); }
// Adapter
public class PrinterAdapter : IPrinter
{
private readonly LegacyPrinter _legacy;
public PrinterAdapter(LegacyPrinter legacy) => _legacy = legacy;
public void Print(string text) => _legacy.PrintText(text);
}
Patrón Decorator
El patrón Decorator permite añadir nuevos comportamientos dinámicamente a objetos existentes. Es un enfoque basado en la composición en lugar de la herencia.
public interface IMessage { string GetText(); }
public class SimpleMessage : IMessage
{
private readonly string _text;
public SimpleMessage(string text) => _text = text;
public string GetText() => _text;
}
// Decorator
public class EncryptedMessage : IMessage
{
private readonly IMessage _inner;
public EncryptedMessage(IMessage inner) => _inner = inner;
public string GetText() => Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(_inner.GetText()));
}
Ventajas
- Factory: Abstrae la creación de objetos, aumenta la flexibilidad.
- Singleton: Gestiona recursos compartidos con una única instancia.
- Repository: Abstrae el acceso a datos, mejora la capacidad de prueba.
- Observer: Establece relaciones publicador–suscriptor, garantizando bajo acoplamiento.
- Strategy: Hace que los algoritmos sean intercambiables.
- Adapter: Integra sistemas incompatibles.
- Decorator: Permite añadir comportamientos de forma dinámica.
TL;DR
- Factory: Abstrae la creación de objetos.
- Singleton: Garantiza una única instancia.
- Repository: Abstrae el acceso a datos.
- Observer: Notifica a los suscriptores sobre los cambios.
- Strategy: Los algoritmos pueden intercambiarse fácilmente.
- Adapter: Adapta clases incompatibles.
- Decorator: Añade características dinámicamente.
Ejemplo: Creación de productos, listado, flujo de pedidos y registro
El siguiente ejemplo muestra juntos la creación de productos (Factory), el listado (Repository),
la creación de pedidos y las actualizaciones de estado (Observer), el pago (Strategy),
la impresión de recibos (Adapter) y el procesamiento de mensajes (Decorator).
Utiliza una configuración Singleton segura con Lazy, una fábrica basada en enum, EventArgs personalizados,
y un formateo de moneda sensible a la cultura.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
// =======================
// DOMINIO (Modelo pequeño)
// =======================
// Expresar tipos de productos con un enum en lugar de cadenas para reducir errores.
public enum ProductType { Book, Phone }
// Tipo record inmutable para transportar datos
public record Product(int Id, string Name, decimal Price);
// =======================
// REPOSITORIO (Almacenamiento)
// =======================
public interface IProductRepository
{
void Add(Product p);
Product? GetById(int id);
IEnumerable<Product> GetAll(); // Exponer IEnumerable; no exponer la lista interna.
}
public class InMemoryProductRepository : IProductRepository
{
private readonly List<Product> _list = new();
public void Add(Product p) => _list.Add(p);
public Product? GetById(int id) => _list.FirstOrDefault(x => x.Id == id);
// Si desea devolver una copia protectora: return _list.ToList();
public IEnumerable<Product> GetAll() => _list;
}
// =======================
// FACTORY (Creación de productos)
// =======================
public static class ProductFactory
{
// Creación centralizada: para añadir un nuevo tipo, basta con extender el switch.
public static Product Create(ProductType type, int id)
=> type switch
{
ProductType.Book => new Product(id, $"Libro-{id}", 29.90m),
ProductType.Phone => new Product(id, $"Teléfono-{id}", 599.00m),
_ => throw new ArgumentOutOfRangeException(nameof(type))
};
}
// =======================
// SINGLETON CONFIGURACIÓN
// =======================
// Singleton simple, seguro para hilos, cargado de forma diferida con Lazy<T>.
public sealed class AppSettings
{
private static readonly Lazy<AppSettings> _lazy = new(() => new AppSettings());
public static AppSettings Instance => _lazy.Value;
private AppSettings() { }
public string StoreName { get; init; } = "Mi Tienda";
}
// =======================
// OBSERVER (Modelo de eventos)
// =======================
// EventArgs personalizado: estructura extensible en lugar de una simple cadena.
public sealed class OrderStatusChangedEventArgs : EventArgs
{
public string Status { get; }
public DateTime When { get; }
public OrderStatusChangedEventArgs(string status, DateTime when)
{
Status = status;
When = when;
}
}
public class Order
{
public event EventHandler<OrderStatusChangedEventArgs>? StatusChanged;
public int Id { get; }
public Product Product { get; }
public Order(int id, Product product)
{
Id = id;
Product = product;
}
public void UpdateStatus(string status)
{
Console.WriteLine($"Pedido {Id} estado: {status}");
StatusChanged?.Invoke(this, new OrderStatusChangedEventArgs(status, DateTime.Now));
}
}
// Oyentes de eventos (suscriptores)
public class EmailNotifier
{
public void OnOrderStatusChanged(object? sender, OrderStatusChangedEventArgs e)
=> Console.WriteLine($"[Email] Notificación enviada al cliente: {e.Status} ({e.When:T})");
}
public class SmsNotifier
{
public void OnOrderStatusChanged(object? sender, OrderStatusChangedEventArgs e)
=> Console.WriteLine($"[SMS] Notificación enviada al cliente: {e.Status} ({e.When:T})");
}
// =======================
// STRATEGY (Pago)
// =======================
public interface IPaymentStrategy
{
void Pay(decimal amount);
}
public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"{amount:0.00} pagado con tarjeta de crédito.");
}
public class PayPalPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"{amount:0.00} pagado vía PayPal.");
}
// =======================
// ADAPTER (Impresión de recibos)
// =======================
// API heredada
public class LegacyPrinter
{
public void PrintText(string t) => Console.WriteLine("[LegacyPrinter] " + t);
}
// Nueva interfaz
public interface IPrinter
{
void Print(string text);
}
// Adaptador: conecta la nueva interfaz con la API heredada.
public class PrinterAdapter : IPrinter
{
private readonly LegacyPrinter _legacy;
public PrinterAdapter(LegacyPrinter legacy) => _legacy = legacy;
public void Print(string text) => _legacy.PrintText(text);
}
// =======================
// DECORATOR (Mensaje)
// =======================
public interface IMessage
{
string GetText();
}
public class SimpleMessage : IMessage
{
private readonly string _text;
public SimpleMessage(string text) => _text = text;
public string GetText() => _text;
}
// Decorador que registra el mensaje
public class LoggedMessageDecorator : IMessage
{
private readonly IMessage _inner;
public LoggedMessageDecorator(IMessage inner) => _inner = inner;
public string GetText()
{
var text = _inner.GetText();
var preview = text.Length > 60 ? text.Substring(0, 60) + "..." : text;
Console.WriteLine("[Log] Mensaje creado (vista previa): " + preview);
return text;
}
}
// Decorador simple que "cifra" el mensaje con Base64
public class EncryptedMessageDecorator : IMessage
{
private readonly IMessage _inner;
public EncryptedMessageDecorator(IMessage inner) => _inner = inner;
public string GetText()
{
var raw = _inner.GetText();
return Convert.ToBase64String(Encoding.UTF8.GetBytes(raw));
}
}
// =======================
// APLICACIÓN (Flujo de demostración)
// =======================
class Program
{
static void Main()
{
// Formateo de moneda con cultura es-ES
var esES = new CultureInfo("es-ES");
Console.WriteLine($"Bienvenido a: {AppSettings.Instance.StoreName}");
Console.WriteLine();
// Agregar productos vía Repository + Factory
IProductRepository repo = new InMemoryProductRepository();
var p1 = ProductFactory.Create(ProductType.Book, 1);
var p2 = ProductFactory.Create(ProductType.Phone, 2);
repo.Add(p1);
repo.Add(p2);
// Listar productos (precio sensible a la cultura)
Console.WriteLine("Productos en stock:");
foreach (var p in repo.GetAll())
Console.WriteLine($"- {p.Id}: {p.Name} {p.Price.ToString("C", esES)}");
Console.WriteLine();
// Crear pedido y suscribirse a eventos
var order = new Order(1001, p1);
var email = new EmailNotifier();
var sms = new SmsNotifier();
// Agregar oyentes a los eventos
order.StatusChanged += email.OnOrderStatusChanged;
order.StatusChanged += sms.OnOrderStatusChanged;
// Pago (Strategy)
IPaymentStrategy payment = new CreditCardPayment();
Console.WriteLine("Procesando pago:");
payment.Pay(order.Product.Price);
Console.WriteLine();
// Ciclo de vida del pedido (Observer activado)
order.UpdateStatus("Aprobado");
order.UpdateStatus("Enviado");
// (Opcional) Cancelar la suscripción del oyente SMS
order.StatusChanged -= sms.OnOrderStatusChanged;
order.UpdateStatus("Entregado");
Console.WriteLine();
// Imprimir recibo (API heredada a través del Adaptador)
IPrinter printer = new PrinterAdapter(new LegacyPrinter());
printer.Print($"Recibo del pedido #{order.Id}: {order.Product.Name} - {order.Product.Price.ToString("C", esES)}");
Console.WriteLine();
// Procesamiento de mensajes (Decorator)
// Objetivo: registrar el texto plano y luego cifrarlo antes de almacenarlo/enviarlo.
// Por lo tanto, encadenar primero Logged y luego Encrypted.
IMessage msg = new SimpleMessage($"¡Gracias por su compra: {order.Product.Name}!");
msg = new LoggedMessageDecorator(msg); // 1) Log: registrar el TEXTO PLANO
msg = new EncryptedMessageDecorator(msg); // 2) Cifrar: salida de texto cifrado
var finalText = msg.GetText();
Console.WriteLine("Mensaje final a almacenar/enviar (Base64):");
Console.WriteLine(finalText);
Console.WriteLine();
// Cambiar estrategia en tiempo de ejecución
payment = new PayPalPayment();
Console.WriteLine("Segundo pago con una estrategia diferente:");
payment.Pay(199.99m);
Console.WriteLine("\nDemostración completada.");
}
}
Artículos relacionados
Encapsulación, Herencia y Polimorfismo en C#
Aprende encapsulación, herencia y polimorfismo en C# con ejemplos claros para dominar los principios básicos de la POO.
Fundamentos de Inyección de Dependencias en C#
Aprende los fundamentos de Inyección de Dependencias en C#, gestionando dependencias y logrando bajo acoplamiento.
Interfaces y Clases Abstractas en C#
Aprende interfaces y clases abstractas en C#, sus diferencias y cuándo usar cada una para diseñar código limpio y extensible.
Principios SOLID en C#
Aplicación de los principios SOLID en C# con ejemplos: código más flexible, mantenible y comprobable.