Patrones de diseño con TypeScript: Strategy

Aprende como funciona y como implementar el patrón de diseño strategy en tus proyectos.

Luis Angel Mendoza Lucio

Pongamos por caso que tenemos una bonita cafetería, donde nuestros clientes pueden pagar su café con distintos métodos de pago: en efectivo o con tarjeta de crédito. Inicialmente, tenemos nuestro código así.

class PaymentProcess {
    constructor(amount: number, paymentMethod: string) {
        if (paymentMethod === 'CREDIT_CARD') {
            console.log("Pagando con tarjeta de crédito $" + amount);
        } else if (paymentMethod === 'CASH') {
            console.log("Pagando en efectivo $" + amount);
        } else {
            throw new Error("Invalid payment method");
        }
    }
}

Ahora se nos ha pedido que agreguemos más métodos de pago para la cafetería, ¿Qué tendríamos que hacer? Pues regresar a la clase PaymentProcess y modificarla para agregar las nuevas opciones de pago, esto viola un principio de diseño llamado Open-Closed este principio nos dice que las entidades de software como nuestras clases deben estar cerradas para modificación y abiertas para la extensión, esto significa que el comportamiento de nuestra clase puede cambiar sin tener que modificar directamente el código que controla el flujo de la aplicación.

Ahorita tenemos solo 2 métodos de pago y el código no se ve horrible porque solo tenemos un console.log como ejemplo, pero imagina que tengamos la implementación de cada método de pago y que tengamos 5 métodos de pago distintos... o más.

Lo que vamos a hacer es identificar cuando tengamos diferentes comportamientos para un mismo punto, en nuestro caso tenemos diferentes formas de pago. Con esto ya nos debería dar una pista para utilizar el patrón strategy.

Primero vamos a crear una interface (dependiendo el caso pueden ser clases abstractas) para definir la estructura que tendrán todos nuestros métodos de pago, que ahora llamaré estratégias.

export interface PaymentMethodStrategy {
  pay(mount: number);
}

¿Por qué hacer esto? sabemos que queremos tener varios métodos de pago, entonces por cada uno vamos a crear una clase concreta, por ejemplo CreditCardPaymentMethod, CashPaymentMethod..etc, lo que no queremos es llegar a algo así:

export class PaymentProcess {
  private paymentMethod: PaymentMethodStrategy;
  constructor(type: string) {
    if (type === 'CREDIT_CARD') {
      this.paymentMethod = new CreditCardPaymentMethod();
    } else if (type === 'CASH') {
      this.paymentMethod = new CreditCardPaymentMethod();
    }
    // ... todos los métodos de pago diferentes
  }
}

Porque de ser así volveremos al mismo problema del código inicial, cada que agreguemos un nuevo método de pago, tendríamos que regresar a la clase PaymentProcess para modificarla y esta clase que solo debe manejar el flujo sin importar el método elegido se empezará a hacer más y más grande y complicada. Pero si tratamos a todas nuestras clases concretas como tipo PaymentMethodStrategy ahora la clase PaymentProcess podrá trabajar sin importarle que estrategia concreta sea la que se va a implementar porque para PaymentProcess todas las clases son del tipo PaymentMethodStrategy y por ende todas las clases deberían tener el método pay().

... Sigamos entonces. Cada método de pago nuevo que tengamos tendrá que implementar esa interface.

export class CreditCardPaymentMethod implements PaymentMethodStrategy {
  pay(amount: number) {
    console.log('Pagando con tarjeta de crédito $' + amount);
  }
}

export class CashPaymentMethod implements PaymentMethodStrategy {
  pay(amount: number) {
    console.log('Pagando con efectivo $' + amount);
  }
}

export class DebitCardPaymentMethod implements PaymentMethodStrategy {
  pay(amount: number) {
    console.log('Pagando con tarjeta de débito $' + amount);
  }
}

export class PaypalPaymentMethod implements PaymentMethodStrategy {
  pay(amount: number) {
    console.log('Pagando con paypal $' + amount);
  }
}

Como vemos agregamos ya varios métodos de pago, cada uno se encargará de implementar su funcionalidad, separada de las demás.

Ahora vamos a ver como utilizamos esas estrategias que acabamos de crear, porque además de mantener separada la lógica de cada estrategia, el patrón strategy nos ayuda a hacerlas intercambiables.

export class PaymentProcess {
  private paymentMethod: PaymentMethodStrategy;
  constructor(paymentMethod: PaymentMethodStrategy) {
    this.paymentMethod = paymentMethod;
  }

  setPaymentMethod(paymentMethod: PaymentMethodStrategy) {
    this.paymentMethod = paymentMethod;
  }

  pay(amount: number) {
    this.paymentMethod.pay(amount);
  }
}

La clase PaymentProcess suele llamarse la clase contexto es quien utiliza las estrategias que creamos. Tenemos una variable de tipo PaymentMethodStrategy porque necesitamos hacer uso del polimorfismo (Recordar que todas nuestras estrategias concretas implementan una interface, que los hace ser de ese mismo tipo) tratamos a todas nuestras estrategias solo como PaymentMethodStrategy, ahí es donde ocurre la magia, tratando a nuestras estrategias como un solo tipo abstracto, sin importar su tipo concreto podemos hacer que la clase PaymentProcess trabaje de igual forma sin importar el método de pago seleccionado, y aunque agreguemos 100 métodos de pago más, esta clase PaymentProcess encargada de hacer los pagos no volverá a ser modificada. ¡Esto es genial! Y de esa manera el principio de diseño Open-Closed se está aplicando, ¿Por qué? Porque podemos expandir nuestra funcionalidad agregando nuevos métodos de pago, pero por cada método nuevo no vamos a volver a modificar a la clase que se encarga del proceso de pago PaymentProcess.

class Main {
  constructor() {
    // ... Se selecciona el cafe deseado
    // ... Se selecciona el método de pago
    this.stepPayment();
  }

  stepPayment() {
    let paymentProcess: PaymentProcess = new PaymentProcess(new CreditCardPaymentMethod());
    paymentProcess.pay(40); // Pagando con tarjeta de crédito $40
    paymentProcess.setPaymentMethod(new CashPaymentMethod());
    paymentProcess.pay(40); // Pagando con efectivo $40
  }
}

Listo, ya hemos aplicado una solución al problema utilizando el patrón de diseño strategy y solo para no dejar el post sin algo de teoría, vamos a ver algo de información sobre este patrón que es importante conocer.

¿Simple verdad? El diagrama UML de este patrón es muy sencillo, solo separamos las diferentes lógicas para realizar una acción sin olvidar que implemente de una interface para luego valernos del polimorfismo (puedes conocer más sobre el polimofismo en el siguiente enlace) para que nuestro patrón funcione. Y pues la clase contexto que puede ser cualquier clase que utilice nuestras estrategias.

¿Qué es Polimorfismo?
¿Qué es el polimorfismo y cómo puede mejorar tu código? Aprende a implementar el polimorfismo en tu propio código y descubre cómo puede simplificar y mejorar tu código.

El patrón de diseño strategy, también conocido como estrategia, es un patrón de diseño de software que pertenece a la categoría de los patrones de comportamiento. Su objetivo principal es permitir la selección de un algoritmo particular entre varios disponibles de forma dinámica durante tiempo de ejecución.

El patrón de diseño strategy se utiliza cuando se tiene un conjunto de algoritmos que realizan una tarea similar, pero difieren en su implementación específica. En lugar de tener un código con múltiples sentencias condicionales para elegir el algoritmo adecuado, se utiliza este patrón para encapsular cada algoritmo en una clase separada y hacer que todas ellas sigan una interface común.

Programación

Luis Angel Mendoza Lucio

Líder de proyectos de frontend, aunque es una persona reservada, le encanta compartir su conocimiento y experiencia con otros, también es ávido lector, y disfruta investigar sobre la historia.

Comentarios