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');
}
}
}
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');
}
}
Este enfoque revela varias desventajas significativas que merecen atención:
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.
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.
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>;
}
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);
}
}
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
}
}
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);
}
}
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);
}
}
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)
Nice example of a strategy pattern. Thanks.