Estrategias Avanzadas: Cómo Implementar el Patrón Strategy en Tus Proyectos NestJS
Tags: nestjs, typescript, design, español
Published: 2024-02-07 22:36:20

[LinkedIn](https://www.linkedin.com/in/fredydlemus/)
[Github](https://github.com/fredydlemus)
[Instagram](https://www.instagram.com/fredydlemus/?next=%2F)

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:

```typescript
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');
    }
  }
}

```
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:
```typescript
...
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');
  }
}

```

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.

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

```
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.
```typescript
// 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);
  }
}

```
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.

```typescript
// 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
  }
}

```
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.
```typescript
// 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);
  }
}

```
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:

```typescript
// 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);
  }
}

```
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.