Singleton y Observer: patrones de proyecto en JavaScript

Singleton y Observer: patrones de proyecto en JavaScript

Los patrones de diseño surgieron en la década de 1990 como respuesta a la necesidad de soluciones estandarizadas a problemas comunes de diseño de software. El libro "Design Patterns: Elements of Reusable Object-Oriented Software", publicado en 1994 por el grupo conocido como "Gang of Four", formalizó y popularizó los patrones de proyecto.

Estos estándares proporcionan pautas comprobadas para diseñar sistemas orientados a objetos, promoviendo la reutilización, flexibilidad y mantenibilidad del código. Desde entonces, los patrones de diseño se han convertido en una parte fundamental de la ingeniería de software y ayudan a los desarrolladores a crear sistemas de software más flexibles, extensibles y mantenibles siguiendo las mejores prácticas y estableciendo un lenguaje de diseño común.

Los patrones de diseño promueven la modularidad, la separación de responsabilidades y la reutilización de código, lo que contribuye a la creación de software de alta calidad.


En este artículo presentaremos al lector algunos patrones de diseño para que pueda comprender ampliamente cómo pueden ser útiles, independientemente de la implementación o características particulares de cada patrón. Con esto, se espera que el lector busque conocer por su cuenta otros patrones de diseño existentes. En este artículo hablaremos sólo de 2 de ellos: Singleton y Observer.

Hay algunas discusiones en la comunidad sobre si se deben utilizar o no algunos patrones. Mi recomendación es que no pierdas tiempo con esto, sobre todo si eres nuevo en el área, al fin y al cabo todos tenemos un punto de vista y aunque todavía no entiendas bien los patrones de diseño, te sugiero que vayas a conócelos y forma tu propia opinión. La idea es que el lector no esté apegado al código, sino al propósito de cada uno de estos estándares. Hay que decir esto porque aunque el autor de este artículo utilizó el paradigma orientado a objetos, se pueden implementar con el paradigma funcional.

Los patrones de diseño se dividen de la siguiente manera:

Patrones de diseño según propósito y objeto (en portugués).


Singleton

El primer patrón de diseño del que vamos a hablar es muy sencillo de entender e implementar, Singleton. Ese designer pattern busca garantizar que solo haya una instancia de una clase y así proporcionar solo un punto de acceso global para esa instancia. Esto es útil en situaciones en las que tener varias instancias de la misma clase puede causar problemas o desperdiciar recursos, por ejemplo, una conexión de base de datos, quizás el ejemplo de caso de uso más clásico para este patrón de diseño.

Imagine que cada vez que se realiza una solicitud HTTP al servidor, se crea una nueva instancia de conexión a la base de datos. Esto podría generar una sobrecarga significativa de la base de datos, ya que cada instancia de conexión requeriría recursos de red y de procesamiento. Además, la creación y destrucción continua de instancias de conexión podría provocar retrasos y afectar negativamente al rendimiento de la aplicación.

1) Crea un archivo singleton.js e inserte el siguiente código:

class DatabaseConnection {

constructor() {

// Simulando la base de datos como un array de usuarios

this.users = [];

}

// Método para agregar un usuario a la base de datos

addUser(user) {

this.users.push(user);

}

// Método para remover un usuario de la base de datos

removeUser(user) {

const index = this.users.findIndex(u => u.id === user.id);

if (index !== -1) {

this.users.splice(index, 1);

}

}

// Método para obtener todos los usuarios de la base de datos

getUsers() {

return this.users;

}

}

class SingletonDatabaseConnection {

constructor() {

if (!SingletonDatabaseConnection.instance) {

SingletonDatabaseConnection.instance = new DatabaseConnection();

}

}

getInstance() {

return SingletonDatabaseConnection.instance;

}

}

// Clase que representa un usuario

class User {

constructor(id, name) {

this.id = id;

this.name = name;

}

}

// Uso de Singleton para obtener la instancia de la conexión con la base de datos

// Instanciando la SingletonDatabaseConnection() dos veces, podemos probar que apenas una instancia fue creada

const connection1 = new SingletonDatabaseConnection().getInstance();

const connection2 = new SingletonDatabaseConnection().getInstance();

// Ambas conexiones son la misma instancia, es decir, no fue creada otra instancia.

console.log(connection1 === connection2); // true

// Creación de usuarios

const user1 = new User(1, 'John');

const user2 = new User(2, 'Alice');

// Vamos a manipular las conexiones. Insertamos el user1 con la connection1 y el user2 con la connection2

connection1.addUser(user1);

connection2.addUser(user2);

// Al llamar el método getUsers de la connection1, recibiremos também el user2 insertado en la connection2.

// probamos así que la connection1 y connection2 son la misma instancia y manipulan los mismos datos.

console.log(connection1.getUsers()); // [User { id: 1, name: 'John' }, User { id: 2, name: 'Alice' }]

// Para finalizar, removemos el user1 por la instancia connection2

connection2.removeUser(user1);

// Al llamar el getUser como connection1, podemos ver que el método removeUser de la connection2 removió el user1

console.log(connection1.getUsers()); // [User { id: 2, name: 'Alice' }]


2) Ejecuta en la terminal:

node singleton.js

3) La salida debe mostrar:

true

[ User { id: 1, name: 'John' }, User { id: 2, name: 'Alice' } ]

[ User { id: 2, name: 'Alice' } ]

Casos de uso para Singleton

- Conexiones con bases de datos: Singleton se puede utilizar para garantizar que solo se establezca y reutilice una única conexión de base de datos en toda la aplicación, evitando la sobrecarga de abrir y cerrar conexiones repetidamente.

- Gestión de configuración global del sistema: Singleton se puede utilizar para almacenar y proporcionar acceso a la configuración global del sistema, como valores de propiedad o preferencias, a los que se debe poder acceder de manera consistente en múltiples componentes de la aplicación.

- Implementación de manejadores de caché centralizados: Con Singleton, puede crear un administrador de caché centralizado que controle el acceso y la actualización de los datos almacenados en caché, garantizando que solo se mantenga una única instancia de caché en todo el sistema.

- Registro de log centralizado: Se puede utilizar un Singleton para crear un registro centralizado, donde todos los mensajes de registro del sistema se registran de manera consistente y se puede acceder a ellos globalmente para fines de depuración, monitoreo o auditoría.

- Administradores de recursos compartidos, como objetos de caché: Singleton se puede aplicar para crear y administrar recursos compartidos, como objetos de caché, garantizando que solo se mantenga y sea accesible una única instancia de estos recursos en toda la aplicación.

Observaciones sobre Singleton

El uso del patrón Singleton se discute en la comunidad de desarrollo de software debido a algunas consideraciones:

  • Acoplamiento y dependencias ocultas: Singleton crea un acoplamiento estrecho entre componentes, lo que dificulta el reemplazo y las pruebas unitarias.
  • Testabilidad: El estado global compartido de Singleton dificulta probar componentes que dependen de él. La inyección de dependencia puede ser un enfoque más comprobable.
  • Multithreading y concurrencia: Es necesario garantizar que la implementación de Singleton sea thread-safe para evitar problemas de concurrencia.
  • Escalabilidad: Singleton puede limitar la escalabilidad en sistemas distribuidos o sistemas que necesitan múltiples instancias.
  • Responsabilidad única: El Singleton debe tener una responsabilidad clara para evitar violar el Principio de Responsabilidad Única (SOLID).
  • En resumen, Singleton tiene sus ventajas, pero es importante sopesar los impactos y considerar alternativas antes de usarlo. Evaluar las necesidades del sistema y los principios de diseño para tomar la mejor decisión.

Observer

El patrón Observer permite que un objeto informe automáticamente a otros objetos sobre cambios relevantes, sin que tengan que verificar constantemente el objeto observado. Imagina un sitio web de noticias que te interesa y te suscribes para recibir nuevas noticias por correo electrónico.

Los observadores se suscriben al objeto observable y reciben notificaciones cuando ocurren cambios importantes. Así, el patrón Observer facilita una comunicación eficiente y desacoplada entre objetos, asegurando que reaccionen de acuerdo a sus necesidades específicas.

1)  Crea un archivo observer.js e inserta el código abajo:

// Classe Observer (Subscriber/Usuário)

class SubscriberUser {

constructor(email) {

this.email = email;

}

// Método llamado cuando se envía una nueva newsletter

update(newsletter) {

console.log(`Enviando e-mail para ${this.email}: Nueva newsletter "${newsletter}" disponible.`);

// Lógica real de envío de e-mail aquí

}

}

// Clase Observable (NewsletterSystem)

class NewsletterSystem {

constructor() {

this.subscribers = [];

}

// Método para inscribir un firmante

subscribe(subscriber) {

this.subscribers.push(subscriber);

}

// Método para cancelar la inscripción de un firmante

unsubscribe(subscriber) {

this.subscribers = this.subscribers.filter((sub) => sub !== subscriber);

}

// Método para enviar una nueva newsletter a todos los firmantes

sendNewsletter(newsletter) {

this.subscribers.forEach((subscriber) => {

subscriber.update(newsletter);

});

}

}

// Ejemplo de uso

const newsletterSystem = new NewsletterSystem();

// Creando firmantes

const subscriber1 = new SubscriberUser('email1@example.com');

const subscriber2 = new SubscriberUser('email2@example.com');

const subscriber3 = new SubscriberUser('email3@example.com');

// Inscribiendo firmantes en el sistema de newsletters

newsletterSystem.subscribe(subscriber1);

newsletterSystem.subscribe(subscriber2);

newsletterSystem.subscribe(subscriber3);

// Enviando una nueva newsletter

newsletterSystem.sendNewsletter('Novidades da Semana');

2) Ejecuta:

node observer.js

3) La salida debe mostrar:

Enviando e-mail para email1@example.com: Nueva newsletter "Novedades de la Semana" disponible.

Enviando e-mail para email2@example.com: Nueva newsletter "Novedades de la Semana" disponible.

Enviando e-mail para email3@example.com: Nueva newsletter "Novedades de la Semana" disponible.

Casos de uso para Observer

  • Botones de inscripción en un site: Se pueden suscribir varios objetos para recibir notificaciones cuando se hace clic en un botón.
  • Monitoreo de temperatura: Los sensores observables pueden notificar automáticamente a un objeto primario los cambios de temperatura, lo que permite tomar las acciones adecuadas.
  • Actualizaciones de feeds de noticias: Los usuarios pueden suscribirse para recibir notificaciones de nuevas noticias de diferentes fuentes. Cada fuente notifica automáticamente a los usuarios.


Observaciones sobre Observer

  • Overhead de desempeño: El patrón Observer puede tener cierta sobrecarga de rendimiento, especialmente en los casos en los que hay una gran cantidad de observadores y/o notificaciones frecuentes. Notificar a cada observador puede requerir recursos y procesamiento adicionales, lo que puede afectar la eficiencia general del sistema.
  • Riesgo de vaciamientos de memoria: Si los objetos de observación no se cancelan correctamente o se eliminan cuando ya no son necesarios, puede ocurrir una pérdida de memoria. Esto puede suceder cuando los objetos observados todavía están registrados en el objeto observable, incluso si ya no son relevantes.
  • Complejidad adicional: La implementación del patrón Observer puede agregar complejidad adicional al código, especialmente si hay múltiples eventos o estados que deben observarse. Esto puede hacer que el código sea más difícil de entender y mantener, especialmente para proyectos grandes.
  • Es importante evaluar estas desventajas frente a los beneficios que el patrón Observer aporta al sistema. En muchos casos, los beneficios superan las desventajas, pero es importante considerar cuidadosamente la aplicación del patrón y el impacto que puede tener en el rendimiento y la complejidad del sistema.


Consideraciones finales

Más importante que comprender cómo implementar patrones de diseño es comprender su propósito de una manera más abstracta, ya que los patrones de diseño se pueden aplicar independientemente del paradigma o lenguaje de programación utilizado, problemas y soluciones.

💡
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Listopro.

Listopro Community da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.