DEV Community

Cover image for Estrategias Avanzadas: Cómo Implementar el Patrón Strategy en Tus Proyectos NestJS

Estrategias Avanzadas: Cómo Implementar el Patrón Strategy en Tus Proyectos NestJS

LinkedIn
Github
Instagram

Uno de los patrones de diseño que más me llamó la atención al aprender TypeScript fue el Strategy Pattern. Aunque no es exclusivo de TypeScript, este patrón enriquece significativamente el código al promover la reusabilidad y el desacoplamiento.

En que consiste el patrón?

El patrón Strategy permite que una clase modifique el algoritmo o comportamiento que emplea durante la ejecución. Esto se logra definiendo una serie de algoritmos, cada uno encapsulado en su propia clase y siguiendo una interfaz común. De esta manera, se pueden intercambiar comportamientos de forma sencilla sin necesidad de alterar la clase que los implementa.

Un ejemplo en la vida real

Imagina una aplicación que envía notificaciones a los usuarios, con métodos de notificación variables, como correo electrónico, SMS o notificaciones push. En lugar de codificar un único método de envío, tu aplicación puede adoptar diferentes estrategias de notificación según las necesidades del momento. Esto se consigue mediante la implementación de una interfaz común para las estrategias de notificación, asegurando que tu aplicación sepa cómo enviarlas, independientemente del método elegido. Este enfoque permite cambiar los métodos de notificación fácilmente, sin afectar el funcionamiento general de la aplicación.
Al principio suena un poco confuso, así que mejor vayamos a un ejemplo con código

Código donde no se esta usando Strategy Pattern

En el contexto del ejemplo anterior y utilizando NestJS, consideremos el servicio NotificationService. Este servicio es responsable de enviar distintos tipos de notificaciones, seleccionando el método apropiado basado en un argumento pasado al método notify. La selección del método de notificación se realiza mediante condicionales, determinando si se envía un correo electrónico, un SMS o una notificación push según el caso:

import { Injectable } from '@nestjs/common';

@Injectable()
export class NotificationService {
  async notify(user: User, message: string, method: string): Promise<void> {
    if (method === 'email') {
      console.log(`Sending email to ${user.email}: ${message}`);
      // Lógica para enviar email
    } else if (method === 'sms') {
      console.log(`Sending SMS to ${user.phone}: ${message}`);
      // Lógica para enviar SMS
    } else if (method === 'push') {
      console.log(`Sending push notification: ${message}`);
      // Lógica para enviar notificaciones push
    } else {
      throw new Error('Invalid notification method');
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

El siguiente fragmento de código demuestra cómo utilizaríamos el NotificationService para enviar dos notificaciones, una por correo electrónico y otra por SMS. En este escenario, el método de notificación se decide en tiempo de ejecución, lo que muestra la flexibilidad de nuestro servicio para adaptarse a diferentes requisitos de notificación:

...
export class AppService {
  constructor(private readonly notificationService: NotificationService) {}

  async sendNotification() {
    const user = { email: 'user@example.com', phone: '+1234567890' };
    const message = 'This is a test message.';

    // Decidir el método en tiempo de ejecución
    await this.notificationService.notify(user, message, 'email');
    await this.notificationService.notify(user, message, 'sms');
  }
}

Enter fullscreen mode Exit fullscreen mode

Este enfoque revela varias desventajas significativas que merecen atención:

  1. Alto Acoplamiento: La selección y ejecución de los métodos de notificación están estrechamente integradas dentro de una sola clase o función, complicando la gestión y expansión del código. Este alto grado de acoplamiento reduce la flexibilidad y aumenta la complejidad del mantenimiento.

  2. Violación del Principio de Abierto/Cerrado (OCP): Incorporar un nuevo método de notificación implica modificar la clase NotificationService directamente, añadiendo más condiciones. Esto contraviene el principio OCP, según el cual las entidades de software deben permitir su extensión sin necesidad de modificar su contenido existente, promoviendo así una mayor escalabilidad y mantenibilidad.

  3. Complejidad en las Pruebas: Verificar el correcto funcionamiento de NotificationService se vuelve más arduo. Cada condición necesita ser probada individualmente para confirmar su operatividad, incrementando el esfuerzo y la complejidad de las pruebas."

Refactorizando el código

Para aplicar el patrón Strategy en nuestra refactorización del NotificationService, eliminaremos las condicionales de la clase. En su lugar, modificaremos la clase para que acepte un objeto a través de su constructor. Este objeto debe adherirse a una interfaz específica que incluya un método send. Este método será responsable de ejecutar la lógica de envío para el tipo de notificación correspondiente, permitiendo así que NotificationService delegue la responsabilidad de enviar la notificación, desacoplando el servicio de los detalles de implementación específicos de cada método de envío. De esta manera, NotificationService actúa meramente como un contexto para el envío de notificaciones, sin estar ligado a ningún método de envío en particular.

Empezaremos por definir la interfaz común que deben cumplir todos los objetos destinados a ser utilizados como estrategias de notificación. Esta interfaz garantiza que cualquier estrategia de notificación proporcione una implementación del método send, que es responsable de ejecutar la lógica específica de envío de notificaciones.

// notification.strategy.interface.ts
export interface NotificationStrategy {
  send(user: User, message: string): Promise<void>;
}

Enter fullscreen mode Exit fullscreen mode

Tras definir la interfaz NotificationStrategy, procedemos a integrarla en nuestro NotificationService. Este cambio implica ajustar el constructor de NotificationService para aceptar un objeto que cumpla con NotificationStrategy. Al hacer esto, eliminamos la necesidad de condicionales dentro del servicio, delegando la responsabilidad de la lógica de envío al método send del objeto estrategia inyectado. La función notify del servicio, por lo tanto, se simplifica significativamente, limitándose a invocar send sobre la estrategia proporcionada.

// notification.service.ts
import { Injectable } from '@nestjs/common';
import { NotificationStrategy } from './notification.strategy.interface';

@Injectable()
export class NotificationService {

  constructor(private strategy: NotificationStrategy) {}

  async notify(user: User, message: string): Promise<void> {
    await this.strategy.send(user, message);
  }
}

Enter fullscreen mode Exit fullscreen mode

Finalmente, para completar nuestra implementación del patrón de estrategia, creamos diferentes tipos de estrategias de notificación que NotificationService puede utilizar. Específicamente, desarrollaremos EmailNotificationStrategy y SmsNotificationStrategy, cada una con su respectivo método send. Estas estrategias concretas encapsulan la lógica específica para enviar notificaciones por correo electrónico y SMS, respectivamente.

// email.notification.strategy.ts
import { NotificationStrategy } from './notification.strategy.interface';
import { Injectable } from '@nestjs/common';

@Injectable()
export class EmailNotificationStrategy implements NotificationStrategy {
  async send(user: User, message: string): Promise<void> {
    console.log(`Sending email to ${user.email}: ${message}`);
    // Aquí iría la lógica para enviar el correo electrónico
  }
}

// sms.notification.strategy.ts
import { NotificationStrategy } from './notification.strategy.interface';
import { Injectable } from '@nestjs/common';

@Injectable()
export class SmsNotificationStrategy implements NotificationStrategy {
  async send(user: User, message: string): Promise<void> {
    console.log(`Sending SMS to ${user.phone}: ${message}`);
    // Aquí iría la lógica para enviar el SMS
  }
}

Enter fullscreen mode Exit fullscreen mode

Para maximizar la flexibilidad de NotificationService y permitir que cambie su estrategia de notificación en tiempo de ejecución, introducimos un método setter, setStrategy. Este método permite actualizar la estrategia de notificación que utiliza NotificationService, facilitando la adaptación a diferentes necesidades sin comprometer la instancia actual del servicio.

// notification.service.ts
import { Injectable } from '@nestjs/common';
import { NotificationStrategy } from './notification.strategy.interface';

@Injectable()
export class NotificationService {

  constructor(private strategy: NotificationStrategy) {}

  setStrategy(strategy: NotificationStrategy) {
    this.strategy = strategy;
  }

  async notify(user: User, message: string): Promise<void> {
    await this.strategy.send(user, message);
  }
}

Enter fullscreen mode Exit fullscreen mode

Con la implementación del patrón de estrategia en nuestro sistema de notificaciones, hemos demostrado cómo se puede aumentar significativamente la flexibilidad y la mantenibilidad del código. El siguiente ejemplo ilustra cómo AppService puede cambiar dinámicamente la estrategia de notificación de EmailNotificationStrategy a SmsNotificationStrategy, adaptándose a diferentes necesidades de notificación en tiempo de ejecución:

// app.service.ts
import { Injectable } from '@nestjs/common';
import { NotificationService } from './notification.service';
import { EmailNotificationStrategy } from './email.notification.strategy';
import { SmsNotificationStrategy } from './sms.notification.strategy';

@Injectable()
export class AppService {
  constructor(
    private readonly notificationService: NotificationService,
    private readonly emailStrategy: EmailNotificationStrategy,
    private readonly smsStrategy: SmsNotificationStrategy,
  ) {}

  async sendNotification() {
    const user = { email: 'user@example.com', phone: '+1234567890' };
    const message = 'This is a test message.';

    // Cambiar estrategia según el contexto
    this.notificationService.setStrategy(this.emailStrategy);
    await this.notificationService.notify(user, message);

    // Cambiar a SMS
    this.notificationService.setStrategy(this.smsStrategy);
    await this.notificationService.notify(user, message);
  }
}

Enter fullscreen mode Exit fullscreen mode

Este enfoque no solo valida la versatilidad del patrón de estrategia, sino que también subraya su valor en la creación de aplicaciones modulares y extensibles. Al separar la lógica de notificación en estrategias concretas e intercambiables, facilitamos la adición de nuevos métodos de notificación y mejoramos la capacidad de prueba del sistema. Además, adherimos al principio de abierto/cerrado, permitiendo que nuestro sistema evolucione con cambios mínimos en el código existente."

"En resumen, la aplicación del patrón de estrategia en la gestión de notificaciones en TypeScript con NestJS demuestra cómo los principios de diseño de software pueden llevarse a la práctica para construir sistemas robustos, flexibles y mantenibles. La capacidad de cambiar estrategias en tiempo de ejecución no solo enriquece nuestra aplicación con flexibilidad operativa, sino que también destaca la importancia de un diseño de software pensado para el futuro.

Top comments (1)

Collapse
 
meguiluzortiz profile image
Manuel Eguiluz

Nice example of a strategy pattern. Thanks.