Skip to content

Nidificazione infernale delle istruzioni if all'interno di subscribe

Perché questo è un anti-pattern

La "ramificazione condizionale complessa all'interno di subscribe" è un chiaro anti-pattern in RxJS. È riconosciuto come un pattern di implementazione da evitare per le seguenti ragioni.

1. Va contro la filosofia di progettazione di RxJS

RxJS incoraggia la programmazione dichiarativa. "Cosa fare" dovrebbe essere espresso nella pipeline e i dettagli di "come farlo" non dovrebbero essere scritti all'interno della sottoscrizione.

ts
// ❌ Anti-pattern: programmazione imperativa (procedurale)
userService.getUser(id).subscribe(user => {
  if (user.isActive) {
    if (user.role === 'admin') {
      console.log('Admin user');
    }
  }
});

// ✅ RxJS-like: programmazione dichiarativa
userService.getUser(id).pipe(
  filter(user => user.isActive),
  filter(user => user.role === 'admin'),
  tap(user => console.log('Admin user'))
).subscribe();

2. Stesso livello di problemi degli anti-pattern esistenti

Come gli altri anti-pattern trattati nel Capitolo 10, causa i seguenti problemi:

Anti-patternProblemi principali
Memory leakDimenticanza di unsubscribe
Inferno delle sottoscrizioniSottoscrizioni annidate
If hell all'interno di subscribe ← questa paginaRamificazione condizionale complessa

3. Problemi specifici

  1. Bassa leggibilità - annidamento profondo e intento del codice poco chiaro
  2. Difficile da testare - troppi rami condizionali, con conseguente esplosione dei casi di test
  3. Non riutilizzabile - la logica è intrappolata all'interno delle sottoscrizioni e non può essere utilizzata altrove
  4. Difficile da debuggare - il flusso di valori è difficile da seguire e l'identificazione delle aree problematiche richiede molto tempo
  5. Gestione complessa degli errori - è difficile rintracciare i punti in cui si verificano gli errori

4. Ambito accettabile e ambito da evitare

ts
// ✅ Consentito: semplice ramificazione (1-2 istruzioni if)
subscribe(user => {
  if (user.isAdmin) {
    console.log('Admin user');
  } else {
    console.log('Regular user');
  }
});

// ⚠️ Attenzione: 2-3 annidati
subscribe(user => {
  if (user) {
    if (user.isActive) {
      console.log('Active user');
    }
  }
});

// ❌ Anti-pattern: nidificazione di 3 o più elementi
subscribe(user => {
  if (user) {
    if (user.isActive) {
      if (user.role === 'admin') {
        if (user.permissions.includes('write')) {
          // Questo è un chiaro anti-pattern
        }
      }
    }
  }
});

Criteri

  • 1-2 istruzioni if: accettabili (ma gli operatori sono preferibili)
  • 3 o più annidati: chiaro anti-pattern (deve essere rifattorizzato)
  • Elaborazione asincrona annidata: necessita di refactoring immediato

Pattern comuni errati

Pattern 1: ramificazione condizionale complessa all'interno delle sottoscrizioni

ts
// ❌ Cattivo esempio: nidificazione di istruzioni if all'interno di subscribe
userService.getUser(id).subscribe(user => {
  if (user) {
    if (user.isActive) {
      if (user.role === 'admin') {
        // Elaborazione per l'amministratore
        console.log('Admin user:', user.name);
        adminService.loadAdminData().subscribe(adminData => {
          if (adminData) {
            // ulteriore annidamento...
          }
        });
      } else {
        // Elaborazione per gli utenti regolari
        console.log('Regular user:', user.name);
      }
    } else {
      // Utente inattivo
      console.log('Inactive user');
    }
  } else {
    // Utente non trovato
    console.log('User not found');
  }
});

Problemi con questo codice

  1. 5 livelli di annidamento - riduce fortemente la leggibilità
  2. Più subscribe all'interno di subscribe - rischio di memory leak
  3. Nessuna gestione degli errori - non si sa dove si verifichino gli errori
  4. Non testabile - difficile testare tutti i percorsi
  5. Non riutilizzabile - la logica non può essere usata altrove

Soluzione che utilizza gli operatori condizionali di RxJS

Soluzione 1: ramificazione condizionale tramite filter

Quando usarlo?: restringere i valori, escludere i valori indesiderati

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

// ✅ Buon esempio: separare le condizioni con filter
userService.getUser(id).pipe(
  filter(user => user !== null),           // controllo null
  filter(user => user.isActive),           // controllo attivo
  filter(user => user.role === 'admin'),   // controllo del ruolo
  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)
});

Vantaggi

  • ✅ Ogni condizione è indipendente (facile da leggere)
  • ✅ Ordine chiaro nella pipeline
  • ✅ La gestione degli errori è centralizzata

Soluzione 2: ramificazione a due scelte con iif

Quando usarla?: eseguire diversi Observable a seconda della condizione

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

// ✅ Buon esempio: commutare i flussi in base alle condizioni con iif
userService.getUser(id).pipe(
  switchMap(user =>
    iif(
      () => user.role === 'admin',
      adminService.loadAdminData(),    // per gli amministratori
      userService.loadUserData()       // per l'utente normale
    )
  )
).subscribe(data => console.log('Data:', data));

Vantaggi

  • ✅ Chiara ramificazione tra le due opzioni
  • ✅ Ogni processo è un Observable indipendente
  • ✅ Facile da testare

Soluzione 3: processo di ramificazione tramite partition

Quando usarla?: dividere il flusso in due ed elaborarli separatamente

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

// ✅ Buon esempio: partition divide il flusso in due parti
const [activeUsers$, inactiveUsers$] = partition(
  userService.getUsers(),
  user => user.isActive
);

// Elaborare gli utenti attivi
const processedActive$ = activeUsers$.pipe(
  tap(user => console.log('Active:', user.name)),
  switchMap(user => userService.loadProfile(user.id))
);

// Elaborare gli utenti inattivi
const processedInactive$ = inactiveUsers$.pipe(
  tap(user => console.log('Inactive:', user.name)),
  map(user => ({ ...user, status: 'archived' }))
);

// Unire i due flussi
merge(processedActive$, processedInactive$).subscribe(
  result => console.log('Processed:', result)
);

Vantaggi

  • ✅ Completa separazione tra attivi e inattivi
  • ✅ Possono essere elaborati indipendentemente l'uno dall'altro
  • ✅ Facilità di estensione

Soluzione 4: defaultIfEmpty per il valore predefinito

Quando usarlo?: fornire un valore predefinito se vuoto

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

// ✅ Buon esempio: fornire un valore predefinito se vuoto
userService.getUser(id).pipe(
  filter(user => user !== null),
  defaultIfEmpty({ id: 0, name: 'Guest', role: 'guest' })
).subscribe(user => {
  console.log('User:', user.name);
});

Vantaggi

  • ✅ Non sono necessari controlli su null
  • ✅ Valori predefiniti chiari
  • ✅ Non c'è bisogno di ramificazioni in subscribe

Soluzione 5: gestione degli errori con throwIfEmpty

Quando usarlo?: trattare come errore se vuoto

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

// ✅ Buon esempio: lanciare un errore se vuoto
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);
  }
});

Vantaggi

  • ✅ Gestione chiara degli errori
  • ✅ Completa in pipeline
  • ✅ Gestione dei successi solo in subscribe

Esempio pratico: rifattorizzazione di una logica condizionale complessa

Prima: if hell in subscribe

ts
// ❌ Cattivo esempio: logica condizionale complessa (annidamento in 6 passi)
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) {
                // Elaborazione...
                console.log('Valid item:', item);
              }
            }
          });
        } else {
          console.log('No items found');
        }
      }
    } else {
      console.log('Request failed');
    }
  }
});

Problemi

  • Nidificazione di 6 passi
  • Ulteriore ramificazione condizionale all'interno di forEach
  • Nessuna gestione degli errori
  • Non testabile
  • Gestione inefficiente degli array

Dopo: strutturato con operatori

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

// ✅ Buon esempio: organizzare le condizioni con gli operatori
apiService.fetchData().pipe(
  // 1. verifica l'esistenza dei dati
  filter(data => data !== null),

  // 2. controllare lo stato
  filter(data => data.status === 'success'),

  // 3. controllare l'esistenza del risultato
  filter(data => data.result !== null),

  // 4. espandere l'array
  map(data => data.result.items),
  defaultIfEmpty([]),  // default in caso di array vuoto

  // 5. elaborare ogni elemento singolarmente
  switchMap(items => from(items)),

  // 6. solo gli elementi validi
  filter(item => item.isValid),

  // 7. controllo del prezzo
  filter(item => item.price > 0),

  // 8. log di debug
  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')
});

Miglioramenti

  • ✅ L'annidamento è 0 (tutto piatto)
  • ✅ Ogni condizione è indipendente e facile da leggere
  • ✅ Aggiunta la gestione degli errori
  • ✅ Più facile da testare (ogni operatore può essere testato separatamente)
  • ✅ Gestione efficiente degli array (in streaming con from())

Guida alla selezione dell'operatore condizionale

Il seguente diagramma di flusso aiuta a determinare l'operatore da utilizzare.

CondizioneOperatore da utilizzareMotivo
Filtrare i valorifilter()Passare solo i valori che corrispondono alla condizione
Scegliere tra due opzioniiif()Commutare il flusso in base alla condizione
Dividere il flusso in duepartition()Dividere in due flussi con True/False
Default se vuotodefaultIfEmpty()Usare il valore predefinito se il valore è mancante
Errore se vuotothrowIfEmpty()Lanciare un errore se non c'è alcun valore
Scelte multiple (più di tre)Operatore personalizzatoSe sono richiesti più di tre rami

Fasi della rifattorizzazione

Passo 1: identificare le condizioni

Enumerare tutte le istruzioni if nella sottoscrizione

ts
// Esempio: abbiamo le seguenti condizioni
if (data)                        // ← Condizione 1
if (data.status === 'success')   // ← Condizione 2
if (data.result)                 // ← Condizione 3
if (item.isValid)                // ← Condizione 4
if (item.price > 0)              // ← Condizione 5

Passo 2: classificare le condizioni

  • Condizioni di filtraggiofilter()
  • Biforcazione di due scelteiif()
  • Divisione dello streampartition()
  • Valore predefinitodefaultIfEmpty()
  • Condizione di errorethrowIfEmpty()

Passo 3: conversione in pipeline

Sostituire la condizione con un operatore nella pipeline

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

Passo 4: semplificare la sottoscrizione

La sottoscrizione ha solo effetti collaterali (output del log, manipolazione del DOM, ecc.)

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

Passo 5: scrivere i test

Verificare che ogni operatore funzioni come previsto

ts
// Esempio: testare un 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();
  });
});

Riutilizzo con operatori personalizzati

La logica condizionale complessa può essere riutilizzata se estratta in operatori personalizzati.

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

// Operatore personalizzato: solo utenti amministratori attivi
function filterActiveAdmins<T extends { isActive: boolean; role: string }>() {
  return pipe(
    filter((user: T) => user.isActive),
    filter((user: T) => user.role === 'admin')
  );
}

// Esempio di utilizzo
userService.getUsers().pipe(
  filterActiveAdmins(),
  tap(user => console.log('Active admin:', user.name))
).subscribe();

Vantaggi

  • ✅ Riutilizzabile
  • ✅ Facile da testare
  • ✅ L'intento è chiaro dal nome

Risorse didattiche correlate

Per comprendere questo anti-pattern, vedere anche le seguenti sezioni.

Checklist per evitare gli anti-pattern

Controllare il codice.

markdown
- [ ] Non sono annidati più di tre if all'interno di subscribe
- [ ] I rami condizionali sono gestiti da operatori in pipe
- [ ] subscribe esegue solo effetti collaterali (registrazione, manipolazione del DOM)
- [ ] Il filter è usato per escludere i valori non necessari
- [ ] Il caso vuoto è gestito da defaultIfEmpty o throwIfEmpty
- [ ] Le condizioni complesse sono estratte in un operatore personalizzato
- [ ] La gestione degli errori è effettuata con catchError
- [ ] Non ci sono più sottoscrizioni annidate all'interno di sottoscrizioni

Prossimi passi

Una volta compreso questo anti-pattern, si possono imparare i pattern pratici nelle pagine seguenti.

  1. Errori comuni e come affrontarli - Imparare altri anti-pattern
  2. Capitolo 11: Superare le difficoltà - Superare le difficoltà specifiche di RxJS
  3. Capitolo 13: Pattern pratici - Imparare i pattern di implementazione corretti

Punti chiave

I rami condizionali complessi nelle sottoscrizioni sono un chiaro anti-pattern contro la filosofia di programmazione dichiarativa di RxJS. Usare operatori come filter, iif e partition per gestire le condizioni nella pipeline.

Pubblicato sotto licenza CC-BY-4.0.