Cargando...

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


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