El Singleton en TypeScript: De Principiante a Experto

Fundamentos 🌟

Introducción al Patrón Singleton

¡Imagina un superhéroe único, que solo puede existir en un único ejemplar en todo el universo. ¡Eso es exactamente el patrón Singleton! 🦸‍♂️

¿Por qué necesitamos un héroe único? 🤔

Aquí algunos escenarios comunes:

  • 🎮 Un videojuego con múltiples tablas de puntuación... ¡confusión total!
  • 🏦 Múltiples conexiones a la misma base de datos... ¡qué desperdicio de recursos!
  • ⚙️ Múltiples configuraciones diferentes... ¡es la puerta al caos!

Superpoderes del Singleton

  • 💪 Fuerza de Unicidad: Una sola instancia, como un solo Batman para Gotham
  • 🌟 Visión Global: Accesible desde cualquier lugar, como la Batseñal en el cielo
  • 🎯 Precisión: Un solo punto de verdad, como el único anillo de Sauron

Implementación Básica

El código de nuestro héroe

typescript
1class Logger {
2 private static instance: Logger;
3 private logCount: number = 0;
4
5 private constructor() {
6 console.log('🚀 Logger despertando...');
7 }
8
9 public static getInstance(): Logger {
10 if (!Logger.instance) {
11 Logger.instance = new Logger();
12 }
13 return Logger.instance;
14 }
15
16 public log(message: string): void {
17 this.logCount++;
18 console.log(`📝 [Log #${this.logCount}] ${message}`);
19 }
20
21 public getStats(): string {
22 return `📊 Total de logs: ${this.logCount}`;
23 }
24}

Uso Simple

typescript
1const logger = Logger.getInstance();
2logger.log('¡Nuestro héroe está listo!');
3console.log(logger.getStats());

Técnicas Avanzadas 🚀

Singleton Thread-Safe

¡En un entorno multi-hilo, nuestro héroe necesita una armadura extra! Aquí una implementación thread-safe:

typescript
1class ThreadSafeLogger {
2 private static instance: ThreadSafeLogger;
3 private static instanceLock = false;
4 private logs: string[] = [];
5
6 private constructor() {}
7
8 public static getInstance(): ThreadSafeLogger {
9 if (!ThreadSafeLogger.instanceLock) {
10 ThreadSafeLogger.instanceLock = true;
11 if (!ThreadSafeLogger.instance) {
12 ThreadSafeLogger.instance = new ThreadSafeLogger();
13 }
14 ThreadSafeLogger.instanceLock = false;
15 }
16 return ThreadSafeLogger.instance;
17 }
18}

Singleton con Inicialización Perezosa 🦥

typescript
1class LazyLogger {
2 private static instance: LazyLogger;
3 private config: object;
4
5 private constructor(config: object) {
6 this.config = config;
7 }
8
9 public static getInstance(config?: object): LazyLogger {
10 if (!LazyLogger.instance && config) {
11 LazyLogger.instance = new LazyLogger(config);
12 }
13 if (!LazyLogger.instance) {
14 throw new Error(
15 '¡El Logger necesita configuración en la primera inicialización!',
16 );
17 }
18 return LazyLogger.instance;
19 }
20}

Singleton con Reset para Pruebas 🧪

typescript
1class TestableLogger {
2 private static instance: TestableLogger;
3
4 private constructor() {}
5
6 public static getInstance(): TestableLogger {
7 if (!TestableLogger.instance) {
8 TestableLogger.instance = new TestableLogger();
9 }
10 return TestableLogger.instance;
11 }
12
13 // Método especial para pruebas
14 public static resetInstance(): void {
15 TestableLogger.instance = null;
16 }
17}

Singleton Genérico 🎭

typescript
1class GenericSingleton<T> {
2 private static instances: Map<string, any> = new Map();
3
4 protected constructor() {}
5
6 public static getInstance<T>(this: new () => T): T {
7 const className = this.name;
8 if (!GenericSingleton.instances.has(className)) {
9 GenericSingleton.instances.set(className, new this());
10 }
11 return GenericSingleton.instances.get(className);
12 }
13}
14
15// Uso
16class UserService extends GenericSingleton<UserService> {
17 public getUsers() {
18 return ['user1', 'user2'];
19 }
20}
21
22class ConfigService extends GenericSingleton<ConfigService> {
23 public getConfig() {
24 return { api: 'url' };
25 }
26}

Anti-Patrones y Trampas a Evitar ⚠️

  1. Acoplamiento Fuerte
typescript
1// Evitar ❌
2class BadSingleton {
3 public static getInstance() {
4 // Código...
5 }
6 public doDirectDatabaseOperation() {
7 // Operación directa en BD
8 }
9}
10
11// Preferir ✅
12interface DatabaseOperation {
13 execute(): void;
14}
15
16class GoodSingleton {
17 public static getInstance() {
18 // Código...
19 }
20 public executeOperation(operation: DatabaseOperation) {
21 operation.execute();
22 }
23}
  1. Estado Global Mutable
typescript
1// Evitar ❌
2class MutableSingleton {
3 private static instance: MutableSingleton;
4 public globalState: any = {};
5}
6
7// Preferir ✅
8class ImmutableSingleton {
9 private static instance: ImmutableSingleton;
10 private state: Readonly<any>;
11
12 public getState(): Readonly<any> {
13 return this.state;
14 }
15}

Patrones Complementarios 🤝

Factory + Singleton

typescript
1interface Logger {
2 log(message: string): void;
3}
4
5class LoggerFactory {
6 private static instance: LoggerFactory;
7 private loggers: Map<string, Logger> = new Map();
8
9 private constructor() {}
10
11 public static getInstance(): LoggerFactory {
12 if (!LoggerFactory.instance) {
13 LoggerFactory.instance = new LoggerFactory();
14 }
15 return LoggerFactory.instance;
16 }
17
18 public getLogger(type: 'console' | 'file'): Logger {
19 if (!this.loggers.has(type)) {
20 this.loggers.set(type, this.createLogger(type));
21 }
22 return this.loggers.get(type);
23 }
24
25 private createLogger(type: 'console' | 'file'): Logger {
26 // Creación del logger específico
27 return type === 'console' ? new ConsoleLogger() : new FileLogger();
28 }
29}

Desafío Avanzado 🎯

Crea un Singleton que:

  1. Sea thread-safe
  2. Use inicialización perezosa
  3. Permita reset para pruebas
  4. Integre un sistema de versionado
typescript
1// ¡Tu código aquí!

Para Finalizar 🎬

El Singleton es un patrón poderoso pero requiere reflexión en su implementación. La versión básica es suficiente para casos simples, pero las versiones avanzadas ofrecen mayor robustez y flexibilidad para aplicaciones complejas.

Consejos Pro 💡

  • Usa inicialización perezosa para recursos costosos
  • Piensa siempre en la testeabilidad
  • Prefiere la inyección de dependencias cuando sea posible
  • Documenta bien los casos de uso de tus Singletons

Ejercicios Prácticos 🎮

  1. Implementa un Singleton para gestionar las configuraciones de una aplicación
  2. Crea un Singleton thread-safe con una cola de mensajes
  3. Desarrolla un sistema de caché usando el patrón Singleton

¡Feliz coding! 🚀

Comparte este artículo


Sébastien Timoner

Sébastien TIMONER

Desarrollador Líder
Experto en Desarrollo a Medida
Aix-en-Provence, France

Experto en desarrollo web y gestión de equipos técnicos, me especializo en la creación y optimización de soluciones digitales de alto rendimiento. Gracias a un profundo dominio de tecnologías modernas como React.js, Node.js, TypeScript y Symfony, garantizo el éxito de proyectos SaaS complejos, desde el diseño hasta la implementación, para empresas de diversos sectores, dentro de offroadLabs.

En offroadLabs, ofrezco servicios de desarrollo a medida, combinando experiencia técnica y enfoque colaborativo. Ya sea para crear una solución SaaS innovadora, modernizar una aplicación existente o acompañar el desarrollo de habilidades de un equipo, me comprometo a proporcionar soluciones robustas y eficientes, adaptadas a las necesidades específicas de cada proyecto.

Estoy disponible para proyectos en la zona de Aix-en-Provence o en modalidad totalmente remota.