Skip to content

Infierno de anidamiento de if dentro de subscribe

Por qué esto es un anti-patrón

La "bifurcación condicional compleja dentro de subscribe" es un anti-patrón claro en RxJS. Se reconoce como patrón de implementación a evitar por las siguientes razones.

1. Va contra la filosofía de diseño de RxJS

RxJS recomienda programación declarativa. Deberías expresar "qué hacer" en el pipeline, no escribir los detalles de "cómo hacerlo" dentro de subscribe.

typescript
// ❌ Anti-patrón: Programación imperativa (procedimental)
userService.getUser(id).subscribe(user => {
  if (user.isActive) {
    if (user.role === 'admin') {
      console.log('Admin user');
    }
  }
});

// ✅ Estilo RxJS: Programación declarativa
userService.getUser(id).pipe(
  filter(user => user.isActive),
  filter(user => user.role === 'admin'),
  tap(user => console.log('Admin user'))
).subscribe();

2. Problemas del mismo nivel que otros anti-patrones

Al igual que otros anti-patrones tratados en el Capítulo 10, causa los siguientes problemas:

Anti-patrónProblema principal
Fuga de memoriaOlvido de unsubscribe
Infierno de subscribeSubscribe anidados
Infierno de if dentro de subscribe ← Esta páginaBifurcación condicional compleja

3. Problemas específicos

  1. Baja legibilidad - El anidamiento se profundiza, la intención del código se vuelve poco clara
  2. Difícil de probar - Los casos de prueba explotan con demasiadas bifurcaciones condicionales
  3. No reutilizable - La lógica queda encerrada dentro de subscribe, no se puede usar en otro lugar
  4. Difícil de depurar - Difícil seguir el flujo de valores, lleva tiempo identificar puntos problemáticos
  5. Manejo de errores complejo - Difícil rastrear dónde ocurrió el error

4. Rango aceptable y rango a evitar

typescript
// ✅ Rango aceptable: Bifurcación simple (1-2 sentencias if)
subscribe(user => {
  if (user.isAdmin) {
    console.log('Admin user');
  } else {
    console.log('Regular user');
  }
});

// ⚠️ Requiere atención: 2-3 niveles de anidamiento
subscribe(user => {
  if (user) {
    if (user.isActive) {
      console.log('Active user');
    }
  }
});

// ❌ Anti-patrón: 3 o más niveles de anidamiento
subscribe(user => {
  if (user) {
    if (user.isActive) {
      if (user.role === 'admin') {
        if (user.permissions.includes('write')) {
          // Esto es claramente un anti-patrón
        }
      }
    }
  }
});

Criterios de juicio

  • 1-2 sentencias if: Rango aceptable (aunque preferible con operadores)
  • 3 o más niveles de anidamiento: Anti-patrón claro (debe refactorizarse)
  • Procesamiento asíncrono anidado: Necesita refactorización inmediata

Patrones malos comunes

Patrón 1: Bifurcación condicional compleja dentro de subscribe

typescript
// ❌ Ejemplo malo: Anidamiento de sentencias if dentro de subscribe
userService.getUser(id).subscribe(user => {
  if (user) {
    if (user.isActive) {
      if (user.role === 'admin') {
        // Procesamiento para administradores
        console.log('Admin user:', user.name);
        adminService.loadAdminData().subscribe(adminData => {
          if (adminData) {
            // Más anidamiento...
          }
        });
      } else {
        // Procesamiento para usuarios regulares
        console.log('Regular user:', user.name);
      }
    } else {
      // Usuario inactivo
      console.log('Inactive user');
    }
  } else {
    // Usuario no encontrado
    console.log('User not found');
  }
});

Problemas de este código

  1. 5 niveles de anidamiento - Legibilidad significativamente reducida
  2. subscribe dentro de subscribe - Riesgo de fuga de memoria
  3. Sin manejo de errores - No está claro dónde puede ocurrir un error
  4. Imposible de probar - Difícil probar todos los caminos
  5. Lógica no reutilizable - No se puede usar en otro lugar

Soluciones usando operadores condicionales de RxJS

Solución 1: Bifurcación condicional usando filter

¿Cuándo usar?: Para filtrar valores, excluir valores innecesarios

typescript
import { filter, tap, switchMap } from 'rxjs';

// ✅ Ejemplo bueno: Separar condiciones con filter
userService.getUser(id).pipe(
  filter(user => user !== null),           // Verificación de null
  filter(user => user.isActive),           // Verificación de activo
  filter(user => user.role === 'admin'),   // Verificación de rol
  tap(user => console.log('Admin user:', user.name)),
  switchMap(user => adminService.loadAdminData())
).subscribe({
  next: adminData => console.log('Admin data loaded', adminData),
  error: err => console.error('Error:', err)
});

Beneficios

  • ✅ Cada condición es independiente (fácil de leer)
  • ✅ Orden claro en pipeline
  • ✅ Manejo de errores centralizado

Solución 2: Bifurcación de dos opciones usando iif

¿Cuándo usar?: Para ejecutar Observable diferente según condición

typescript
import { iif, of, switchMap } from 'rxjs';

// ✅ Ejemplo bueno: Cambiar stream con iif según condición
userService.getUser(id).pipe(
  switchMap(user =>
    iif(
      () => user.role === 'admin',
      adminService.loadAdminData(),    // Caso administrador
      userService.loadUserData()       // Caso usuario regular
    )
  )
).subscribe(data => console.log('Data:', data));

Beneficios

  • ✅ Bifurcación de dos opciones clara
  • ✅ Cada procesamiento es Observable independiente
  • ✅ Fácil de probar

Solución 3: Procesamiento de bifurcación usando partition

¿Cuándo usar?: Para dividir stream en dos y procesar por separado

typescript
import { partition, merge, tap, switchMap, map } from 'rxjs';

// ✅ Ejemplo bueno: Dividir stream en dos con partition
const [activeUsers$, inactiveUsers$] = partition(
  userService.getUsers(),
  user => user.isActive
);

// Procesamiento de usuarios activos
const processedActive$ = activeUsers$.pipe(
  tap(user => console.log('Active:', user.name)),
  switchMap(user => userService.loadProfile(user.id))
);

// Procesamiento de usuarios inactivos
const processedInactive$ = inactiveUsers$.pipe(
  tap(user => console.log('Inactive:', user.name)),
  map(user => ({ ...user, status: 'archived' }))
);

// Integrar dos streams
merge(processedActive$, processedInactive$).subscribe(
  result => console.log('Processed:', result)
);

Beneficios

  • ✅ Separación completa de activos/inactivos
  • ✅ Cada uno se puede procesar independientemente
  • ✅ Fácil de extender

Solución 4: Valor por defecto con defaultIfEmpty

¿Cuándo usar?: Para proporcionar valor por defecto cuando está vacío

typescript
import { defaultIfEmpty, filter } from 'rxjs';

// ✅ Ejemplo bueno: Proporcionar valor por defecto cuando está vacío
userService.getUser(id).pipe(
  filter(user => user !== null),
  defaultIfEmpty({ id: 0, name: 'Guest', role: 'guest' })
).subscribe(user => {
  console.log('User:', user.name);
});

Beneficios

  • ✅ No necesita verificación de null
  • ✅ Valor por defecto claro
  • ✅ No necesita bifurcación dentro de subscribe

Solución 5: Manejo de errores con throwIfEmpty

¿Cuándo usar?: Para tratar como error cuando está vacío

typescript
import { throwIfEmpty, catchError, filter } from 'rxjs';
import { of } from 'rxjs';

// ✅ Ejemplo bueno: Lanzar error cuando está vacío
userService.getUser(id).pipe(
  filter(user => user !== null),
  throwIfEmpty(() => new Error('User not found')),
  catchError(err => {
    console.error('Error:', err.message);
    return of(null);
  })
).subscribe(user => {
  if (user) {
    console.log('User found:', user.name);
  }
});

Beneficios

  • ✅ Procesamiento de errores claro
  • ✅ Completo dentro del pipeline
  • ✅ subscribe solo tiene procesamiento de éxito

Ejemplo práctico: Refactorización de lógica condicional compleja

Before: Infierno de if dentro de subscribe

typescript
// ❌ Ejemplo malo: Lógica condicional compleja (6 niveles de anidamiento)
apiService.fetchData().subscribe(data => {
  if (data) {
    if (data.status === 'success') {
      if (data.result) {
        if (data.result.items.length > 0) {
          data.result.items.forEach(item => {
            if (item.isValid) {
              if (item.price > 0) {
                // Procesamiento...
                console.log('Valid item:', item);
              }
            }
          });
        } else {
          console.log('No items found');
        }
      }
    } else {
      console.log('Request failed');
    }
  }
});

Problemas

  • 6 niveles de anidamiento
  • Más bifurcaciones condicionales dentro de forEach
  • Sin manejo de errores
  • Imposible de probar
  • Procesamiento de arrays ineficiente

After: Estructurado con operadores

typescript
import { filter, map, defaultIfEmpty, switchMap, tap } from 'rxjs';
import { from } from 'rxjs';

// ✅ Ejemplo bueno: Organizar condiciones con operadores
apiService.fetchData().pipe(
  // 1. Verificación de existencia de datos
  filter(data => data !== null),

  // 2. Verificación de estado
  filter(data => data.status === 'success'),

  // 3. Verificación de existencia de resultado
  filter(data => data.result !== null),

  // 4. Expandir array
  map(data => data.result.items),
  defaultIfEmpty([]),  // Por defecto array vacío

  // 5. Procesar cada ítem individualmente
  switchMap(items => from(items)),

  // 6. Solo ítems válidos
  filter(item => item.isValid),

  // 7. Verificación de precio
  filter(item => item.price > 0),

  // 8. Log de depuración
  tap(item => console.log('Valid item:', item))

).subscribe({
  next: item => console.log('Processing:', item),
  error: err => console.error('Error:', err),
  complete: () => console.log('All items processed')
});

Mejoras

  • ✅ Anidamiento 0 (todo plano)
  • ✅ Cada condición es independiente y fácil de leer
  • ✅ Se agregó manejo de errores
  • ✅ Fácil de probar (se puede probar cada operador individualmente)
  • ✅ Procesamiento de arrays eficiente (convertido a stream con from())

Guía de selección de operadores condicionales

Con el siguiente diagrama de flujo puedes juzgar qué operador usar.

CondiciónOperador a usarRazón
Filtrar valoresfilter()Solo pasar valores que cumplan condición
Elegir entre dos opcionesiif()Cambiar stream según condición
Dividir stream en dospartition()Dividir en dos streams por True/False
Por defecto cuando vacíodefaultIfEmpty()Usar valor por defecto cuando no hay valor
Error cuando vacíothrowIfEmpty()Lanzar error cuando no hay valor
Múltiples opciones (3 o más)Operador personalizadoCuando se necesitan 3 o más bifurcaciones

Pasos de refactorización

Step 1: Identificar condiciones

Enumerar todas las sentencias if dentro de subscribe

typescript
// Ejemplo: Hay las siguientes condiciones
if (data)                        // ← Condición 1
if (data.status === 'success')   // ← Condición 2
if (data.result)                 // ← Condición 3
if (item.isValid)                // ← Condición 4
if (item.price > 0)              // ← Condición 5

Step 2: Clasificar condiciones

  • Condiciones de filtradofilter()
  • Bifurcación de dos opcionesiif()
  • División de streampartition()
  • Valor por defectodefaultIfEmpty()
  • Condición de errorthrowIfEmpty()

Step 3: Convertir a pipeline

Reemplazar condiciones por operadores dentro de pipe

typescript
.pipe(
  filter(data => data !== null),              // Condición 1
  filter(data => data.status === 'success'),  // Condición 2
  filter(data => data.result !== null),       // Condición 3
  switchMap(data => from(data.result.items)),
  filter(item => item.isValid),               // Condición 4
  filter(item => item.price > 0)              // Condición 5
)

Step 4: Simplificar subscribe

subscribe solo efectos secundarios (salida de logs, operaciones DOM, etc.)

typescript
.subscribe({
  next: item => console.log('Valid item:', item),
  error: err => console.error('Error:', err),
  complete: () => console.log('Complete')
});

Step 5: Escribir pruebas

Verificar que cada operador funciona como se espera

typescript
// Ejemplo: Prueba de filter
it('should filter out null values', () => {
  const source$ = of(null, { id: 1 }, null);
  const result$ = source$.pipe(
    filter(data => data !== null)
  );

  result$.subscribe(data => {
    expect(data).not.toBeNull();
  });
});

Reutilización con operador personalizado

La lógica condicional compleja se puede reutilizar si se extrae a operador personalizado.

typescript
import { pipe } from 'rxjs';
import { filter } from 'rxjs';

// Operador personalizado: Solo usuarios administradores activos
function filterActiveAdmins<T extends { isActive: boolean; role: string }>() {
  return pipe(
    filter((user: T) => user.isActive),
    filter((user: T) => user.role === 'admin')
  );
}

// Ejemplo de uso
userService.getUsers().pipe(
  filterActiveAdmins(),
  tap(user => console.log('Active admin:', user.name))
).subscribe();

Beneficios

  • ✅ Reutilizable
  • ✅ Fácil de probar
  • ✅ Intención clara por nombre

Recursos de aprendizaje relacionados

Para entender este anti-patrón, consulta también las siguientes secciones.

Checklist de evitación de anti-patrones

Verifica tu código.

markdown
- [ ] No hay 3 o más sentencias if anidadas dentro de subscribe
- [ ] Las bifurcaciones condicionales se procesan con operadores dentro de pipe
- [ ] subscribe solo realiza efectos secundarios (logs, operaciones DOM)
- [ ] Se usan filters para excluir valores innecesarios
- [ ] El procesamiento cuando vacío se maneja con defaultIfEmpty o throwIfEmpty
- [ ] Las condiciones complejas se extraen a operador personalizado
- [ ] El manejo de errores se realiza con catchError
- [ ] No hay más subscribes anidados dentro de subscribe

Próximos pasos

Después de entender este anti-patrón, aprende patrones prácticos en las siguientes páginas.

  1. Errores comunes y soluciones - Aprender otros anti-patrones
  2. Capítulo 11: Superación de dificultades - Superar dificultades específicas de RxJS
  3. Capítulo 13: Patrones prácticos - Dominar patrones de implementación correctos (en preparación)

Punto importante

La bifurcación condicional compleja dentro de subscribe es un anti-patrón claro que va contra la filosofía de programación declarativa de RxJS. Usa operadores como filter, iif, partition para procesar condiciones dentro del pipeline.

Publicado bajo licencia CC-BY-4.0.