tutoriales.com

C#: Refactorizando con Patrones de Diseño - Estrategia y Observador en la Práctica

Este tutorial profundiza en la refactorización de código C# utilizando dos patrones de diseño fundamentales: Estrategia y Observador. Aprenderás a identificar áreas de mejora y a aplicar estos patrones para crear soluciones más flexibles, desacopladas y fáciles de mantener. Incluye ejemplos prácticos y paso a paso.

Intermedio25 min de lectura17 views
Reportar error

🚀 Introducción a los Patrones de Diseño y la Refactorización

En el mundo del desarrollo de software, escribir código que funcione es solo la mitad de la batalla. La otra mitad, igualmente crucial, es escribir código que sea mantenible, escalable y fácil de entender. Aquí es donde entran en juego la refactorización y los patrones de diseño.

La refactorización es el proceso de reestructurar el código existente sin cambiar su comportamiento externo. Su objetivo principal es mejorar la legibilidad, la complejidad y la mantenibilidad del software. Por otro lado, los patrones de diseño son soluciones generales y reutilizables a problemas comunes que surgen en el diseño de software. No son una solución directa que puedas copiar y pegar, sino plantillas que deben adaptarse a la situación específica.

En este tutorial, nos centraremos en dos patrones de diseño clave: el patrón Estrategia y el patrón Observador. Estos patrones nos permiten manejar el comportamiento variante y la comunicación entre objetos de una manera limpia y desacoplada, facilitando enormemente la refactorización y la futura evolución de nuestras aplicaciones C#.

💡 Consejo: La refactorización es un proceso continuo. No esperes a que el código sea un "spaghetti" para empezar. Pequeñas mejoras incrementales son mucho más efectivas.

¿Por qué son importantes la refactorización y los patrones de diseño?

  • Mantenibilidad: El código bien estructurado es más fácil de depurar y modificar.
  • Escalabilidad: Añadir nuevas funcionalidades o cambiar las existentes se vuelve menos propenso a errores.
  • Legibilidad: Reduce la curva de aprendizaje para nuevos miembros del equipo.
  • Reusabilidad: Los componentes bien diseñados pueden ser reutilizados en diferentes partes de la aplicación o en otros proyectos.
  • Reducción de la Deuda Técnica: Ayuda a prevenir la acumulación de código difícil de manejar y costoso de mantener.

🎯 Patrón de Diseño Estrategia: Flexibilidad en el Comportamiento

El patrón Estrategia (Strategy) es un patrón de diseño de comportamiento que permite definir una familia de algoritmos, encapsular cada uno como una clase y hacer que sean intercambiables. Esto permite que el algoritmo varíe independientemente de los clientes que lo usan. En esencia, evita múltiples sentencias if-else o switch que verifican el tipo de algoritmo a utilizar.

📖 Conceptos Clave

  • Contexto: La clase que utiliza una de las estrategias. Mantiene una referencia a un objeto Strategy y delega la ejecución del algoritmo a este objeto.
  • Interfaz/Clase Abstracta Estrategia: Define una interfaz común para todos los algoritmos soportados. El Contexto utiliza esta interfaz para llamar al algoritmo definido por una Estrategia concreta.
  • Estrategias Concretas: Implementan la interfaz Strategy, proporcionando su propia implementación del algoritmo.

🛠️ Ejemplo Práctico: Procesamiento de Pagos

Imaginemos un sistema de comercio electrónico que procesa diferentes tipos de pago (tarjeta de crédito, PayPal, transferencia bancaria). Sin el patrón Estrategia, podríamos tener un código como este:

public class ProcesadorPagosAntiguo
{
    public void ProcesarPago(string tipoPago, decimal cantidad)
    {
        if (tipoPago == "TarjetaCredito")
        {
            Console.WriteLine($"Procesando pago con Tarjeta de Crédito por {cantidad:C}");
            // Lógica específica para tarjeta de crédito
        }
        else if (tipoPago == "PayPal")
        {
            Console.WriteLine($"Procesando pago con PayPal por {cantidad:C}");
            // Lógica específica para PayPal
        }
        else if (tipoPago == "TransferenciaBancaria")
        {
            Console.WriteLine($"Procesando pago con Transferencia Bancaria por {cantidad:C}");
            // Lógica específica para transferencia bancaria
        }
        else
        {
            Console.WriteLine($"Tipo de pago '{tipoPago}' no soportado.");
        }
    }
}

// Uso
var procesadorAntiguo = new ProcesadorPagosAntiguo();
procesadorAntiguo.ProcesarPago("TarjetaCredito", 99.99m);

Este código tiene varios problemas:

  • Aislamiento de Responsabilidades: La clase ProcesadorPagosAntiguo tiene demasiadas responsabilidades (decidir qué tipo de pago y ejecutarlo).
  • Open/Closed Principle: Para añadir un nuevo tipo de pago, tenemos que modificar esta clase existente, lo que puede introducir errores.
  • Mantenibilidad: Se vuelve difícil de manejar a medida que se añaden más tipos de pago.

🔄 Refactorizando con el Patrón Estrategia

Vamos a refactorizar este ejemplo usando el patrón Estrategia.

Paso 1: Definir la interfaz Estrategia

public interface IEstrategiaPago
{
    void Pagar(decimal cantidad);
}

Paso 2: Crear Estrategias Concretas

public class PagoTarjetaCredito : IEstrategiaPago
{
    public void Pagar(decimal cantidad)
    {
        Console.WriteLine($"💳 Procesando pago con Tarjeta de Crédito por {cantidad:C}");
        // Lógica real de pasarela de tarjeta de crédito
    }
}

public class PagoPayPal : IEstrategiaPago
{
    public void Pagar(decimal cantidad)
    {
        Console.WriteLine($"💰 Procesando pago con PayPal por {cantidad:C}");
        // Lógica real de integración con PayPal
    }
}

public class PagoTransferenciaBancaria : IEstrategiaPago
{
    public void Pagar(decimal cantidad)
    {
        Console.WriteLine($"🏦 Procesando pago con Transferencia Bancaria por {cantidad:C}");
        // Lógica real de transferencia bancaria
    }
}

Paso 3: Crear la clase Contexto

public class ProcesadorPagos
{
    private IEstrategiaPago _estrategiaPago;

    public ProcesadorPagos(IEstrategiaPago estrategiaPago)
    {
        _estrategiaPago = estrategiaPago ?? throw new ArgumentNullException(nameof(estrategiaPago));
    }

    public void EstablecerEstrategia(IEstrategiaPago estrategiaPago)
    {
        _estrategiaPago = estrategiaPago ?? throw new ArgumentNullException(nameof(estrategiaPago));
    }

    public void EjecutarPago(decimal cantidad)
    {
        _estrategiaPago.Pagar(cantidad);
    }
}

Paso 4: Usar el patrón Estrategia

// Uso del nuevo procesador de pagos
Console.WriteLine("\n--- Usando el patrón Estrategia ---");

// Pago con tarjeta de crédito
var procesadorCC = new ProcesadorPagos(new PagoTarjetaCredito());
procesadorCC.EjecutarPago(120.50m);

// Pago con PayPal
var procesadorPayPal = new ProcesadorPagos(new PagoPayPal());
procesadorPayPal.EjecutarPago(50.00m);

// Cambiar la estrategia en tiempo de ejecución (opcional)
var procesadorDinamico = new ProcesadorPagos(new PagoTarjetaCredito());
procesadorDinamico.EjecutarPago(25.00m);

procesadorDinamico.EstablecerEstrategia(new PagoTransferenciaBancaria());
procesadorDinamico.EjecutarPago(300.75m);
📌 Nota: Ahora, para añadir un nuevo tipo de pago, simplemente creamos una nueva clase que implemente `IEstrategiaPago` y no necesitamos modificar la clase `ProcesadorPagos`. ¡Esto cumple con el Principio Abierto/Cerrado (Open/Closed Principle)!
ProcesadorPagos - estrategia: IEstrategiaPago + Procesar(monto) «Interface» IEstrategiaPago + Pagar(decimal cantidad) PagoTarjetaCredito + Pagar(decimal) PagoPayPal + Pagar(decimal) PagoTransferenciaBancaria + Pagar(decimal)

✨ Patrón de Diseño Observador: Comunicación Desacoplada

El patrón Observador (Observer) es un patrón de diseño de comportamiento que define una dependencia de uno a muchos entre objetos, de manera que cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente. Este patrón es ideal para implementar sistemas de notificación y para lograr un bajo acoplamiento entre objetos.

📖 Conceptos Clave

  • Sujeto (Subject) / Publicador (Publisher): Es el objeto cuyo estado es de interés para otros objetos. Mantiene una lista de observadores y tiene métodos para adjuntar, desadjuntar y notificar a los observadores.
  • Observador (Observer) / Suscriptor (Subscriber): Es el objeto que desea ser notificado de los cambios en el estado del sujeto. Define una interfaz de actualización para recibir notificaciones.
  • Observadores Concretos: Implementan la interfaz Observer y realizan alguna acción cuando son notificados por el sujeto.

🛠️ Ejemplo Práctico: Notificaciones de Stock de Productos

Consideremos un sistema de inventario donde queremos notificar a los clientes interesados cuando un producto vuelve a estar en stock. Sin el patrón Observador, podríamos tener un código donde la clase Producto tiene referencias directas a Cliente y los notifica, lo que lleva a un alto acoplamiento.

🔄 Refactorizando con el Patrón Observador

Vamos a refactorizar este ejemplo para usar el patrón Observador.

Paso 1: Definir la interfaz Observador

public interface IObservadorProducto
{
    void Actualizar(string nombreProducto, int stockActual);
}

Paso 2: Definir el Sujeto (Publicador)

public interface ISujetoProducto
{
    void Adjuntar(IObservadorProducto observador);
    void Desadjuntar(IObservadorProducto observador);
    void NotificarObservadores();
}

public class Producto : ISujetoProducto
{
    private List<IObservadorProducto> _observadores = new List<IObservadorProducto>();
    private string _nombre;
    private int _stock;

    public Producto(string nombre, int stock)
    {
        _nombre = nombre;
        _stock = stock;
    }

    public string Nombre => _nombre;
    public int Stock
    {
        get { return _stock; }
        set
        {
            if (_stock != value)
            {
                _stock = value;
                Console.WriteLine($"Producto '{_nombre}' cambió a stock: {_stock}");
                if (_stock > 0) // Solo notificar cuando hay stock disponible
                {
                    NotificarObservadores();
                }
            }
        }
    }

    public void Adjuntar(IObservadorProducto observador)
    {
        if (!_observadores.Contains(observador))
        {
            _observadores.Add(observador);
            Console.WriteLine($"'{observador.GetType().Name}' se ha suscrito a '{_nombre}'.");
        }
    }

    public void Desadjuntar(IObservadorProducto observador)
    {
        if (_observadores.Remove(observador))
        {
            Console.WriteLine($"'{observador.GetType().Name}' se ha desuscrito de '{_nombre}'.");
        }
    }

    public void NotificarObservadores()
    {
        Console.WriteLine($"Notificando observadores del producto '{_nombre}'...");
        foreach (var observador in _observadores)
        {
            observador.Actualizar(_nombre, _stock);
        }
    }
}

Paso 3: Crear Observadores Concretos

public class ClienteInteresado : IObservadorProducto
{
    private string _nombreCliente;

    public ClienteInteresado(string nombreCliente)
    {
        _nombreCliente = nombreCliente;
    }

    public void Actualizar(string nombreProducto, int stockActual)
    {
        Console.WriteLine($"¡Hola, {_nombreCliente}! El producto '{nombreProducto}' ahora tiene {stockActual} unidades en stock.");
        // Aquí podría enviarse un correo electrónico, una notificación push, etc.
    }
}

public class AdministradorStock : IObservadorProducto
{
    public void Actualizar(string nombreProducto, int stockActual)
    {
        Console.WriteLine($"[ADMIN] Alerta de Stock: '{nombreProducto}' - Unidades disponibles: {stockActual}.");
        // Lógica para el administrador, quizás reordenar stock.
    }
}

Paso 4: Usar el patrón Observador

Console.WriteLine("\n--- Usando el patrón Observador ---");

var telefono = new Producto("Smartphone X", 0);

var cliente1 = new ClienteInteresado("Ana");
var cliente2 = new ClienteInteresado("Pedro");
var admin = new AdministradorStock();

telefono.Adjuntar(cliente1);
telefono.Adjuntar(cliente2);
telefono.Adjuntar(admin);

Console.WriteLine("\n--- Cambio de Stock ---");
telefono.Stock = 5; // Esto notificará a los observadores

Console.WriteLine("\n--- Desuscripción ---");
telefono.Desadjuntar(cliente2);
telefono.Stock = 10; // Solo Ana y el Admin serán notificados
🔥 Importante: El patrón Observador desacopla completamente el sujeto de sus observadores. El `Producto` no necesita saber *quién* está escuchando ni *qué* harán con la notificación. Solo sabe que debe notificar a la interfaz `IObservadorProducto`.
<<interface>> ISujetoProducto + Adjuntar(obs) + Notificar() Producto - observadores: List + Adjuntar(obs) + Notificar() <<interface>> IObservadorProducto + Actualizar(id, precio) ClienteInteresado + Actualizar(...) AdministradorStock + Actualizar(...)

💡 Combinando y Eligiendo el Patrón Correcto

Saber cuándo aplicar cada patrón es clave. A menudo, los patrones pueden complementarse entre sí o usarse en diferentes partes de una misma solución.

Estrategia vs. Observador: ¿Cuándo usar cuál?

CaracterísticaPatrón EstrategiaPatrón Observador
---------
Propósito PrincipalIntercambiar algoritmos o comportamientosNotificar cambios de estado a múltiples dependientes
RelaciónEl Contexto usa una EstrategiaEl Sujeto tiene Observadores
---------
AcoplamientoEl Contexto está acoplado a la interfaz EstrategiaEl Sujeto está acoplado a la interfaz Observador
CambioEl comportamiento del Contexto varíaEl estado del Sujeto cambia
---------
Uso TípicoDiferentes reglas de negocio, algoritmos de cálculo, formatos de exportaciónSistemas de notificación, eventos, UI/modelos, logs
Aplicabilidad del Patrón: 90%

Consideraciones al Refactorizar

  • Identificar "Code Smells": Busca bloques if/else o switch anidados que manejen comportamientos diferentes (candidato para Estrategia), o dependencias directas entre clases que podrían ser notificaciones (candidato para Observador).
  • Principios SOLID: Los patrones de diseño son una excelente manera de adherirse a principios como el Principio de Responsabilidad Única (SRP) y el Principio Abierto/Cerrado (OCP).
  • Complejidad: No apliques patrones por aplicar. Introducen abstracción que, si no es necesaria, puede aumentar la complejidad innecesariamente.
  • Pruebas Unitarias: La refactorización con patrones a menudo facilita las pruebas unitarias al aislar responsabilidades.

Otras Herramientas de Refactorización en C#

Más allá de los patrones, C# y Visual Studio ofrecen potentes herramientas de refactorización:

  • Extraer Interfaz: Permite crear una interfaz a partir de una clase existente.
  • Extraer Método: Convierte un bloque de código en un método separado.
  • Renombrar: Cambia el nombre de variables, métodos y clases de forma segura.
  • Encapsular Campo: Convierte un campo público en una propiedad privada con acceso a través de métodos get/set.
¿Por qué el Principio Abierto/Cerrado es tan importante?El Principio Abierto/Cerrado (Open/Closed Principle - OCP) establece que las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para extensión, pero cerradas para modificación. Esto significa que podemos añadir nuevas funcionalidades sin cambiar el código existente. Los patrones Estrategia y Observador son ejemplos perfectos de cómo lograr el OCP, ya que permiten añadir nuevas estrategias u observadores sin tocar el código de las clases `ProcesadorPagos` o `Producto` respectivamente.

✅ Conclusión y Próximos Pasos

Hemos explorado en profundidad dos patrones de diseño fundamentales, Estrategia y Observador, y hemos visto cómo utilizarlos para refactorizar código C# de una manera que mejora significativamente su flexibilidad, mantenibilidad y escalabilidad. La capacidad de identificar cuándo y cómo aplicar estos patrones es una habilidad invaluable para cualquier desarrollador.

💡 Consejo: Practica estos patrones en tus propios proyectos. Empieza con un código simple y busca oportunidades para aplicar la Estrategia o el Observador.

Recuerda que los patrones de diseño son solo herramientas. La clave es entender los problemas que resuelven y aplicarlos de manera sensata, evitando la sobre-ingeniería.

¡Sigue explorando! Hay muchos otros patrones de diseño (Creacionales, Estructurales, de Comportamiento) que pueden enriquecer aún más tu caja de herramientas de desarrollo en C#. Algunos de ellos incluyen: Factory Method, Singleton, Adapter, Decorator, Command, entre otros.

// Desafío para el lector:
// Intenta implementar un nuevo tipo de pago (e.g., ApplePay) usando el patrón Estrategia.
// O crea un nuevo observador (e.g., un sistema de alerta por SMS) para el sistema de stock.

Console.WriteLine("¡Has completado el tutorial! Sigue practicando.");

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!