Skip to content

Inferno one-liner e sintassi di separazione delle fasi

Il motivo principale per cui il codice RxJS sembra un "inferno one-liner" è che "definizioni degli stream", "trasformazioni" e "sottoscrizioni (side effect)" sono mescolate insieme. Questo riduce significativamente la leggibilità e la facilità di debug.

Perché si verifica l'"inferno one-liner"?

❌ Codice problematico comune

ts
import { fromEvent } from 'rxjs';
import { map, filter, debounceTime, switchMap } from 'rxjs';
import { ajax } from 'rxjs/ajax';

fromEvent(document, 'click')
  .pipe(
    map(ev => (ev as MouseEvent).clientX),
    filter(x => x > 100),
    debounceTime(300),
    switchMap(x => ajax(`/api?x=${x}`))
  )
  .subscribe(res => {
    if (res.status === 200) {
      console.log('OK');
    } else {
      handleError(res);
    }
  });

function handleError(res: any) {
  console.error('Error:', res);
}

Problemi

ProblemaImpatto
Righe troppo lungheI lettori si perdono
Difficile da debuggareDifficile verificare lo stato intermedio
Difficile da testareL'unico modo è testare l'intero stream
Struttura annidataLe ramificazioni condizionali tendono ad essere profonde in subscribe
Non riutilizzabileL'elaborazione della pipeline non può essere usata altrove

Soluzione: sintassi di separazione delle fasi (Functional Style)

Organizzare il codice RxJS in una "struttura a tre fasi con relazioni chiare".

  1. Definizione dello stream (source) - fonte dei dati
  2. Trasformazione dello stream (pipeline) - elaborazione dei dati
  3. Sottoscrizione e side effect (subscription) - side effect come aggiornamenti UI e log

Pattern consigliato: sintassi di separazione delle fasi

ts
import { fromEvent } from 'rxjs';
import { map, filter, throttleTime } from 'rxjs';

// 1. Definizione Observable (fonte dello stream)
const clicks$ = fromEvent(document, 'click');

// 2. Definizione pipeline (elaborazione di trasformazione dei dati)
const processed$ = clicks$.pipe(
  map(event => (event as MouseEvent).clientX),
  filter(x => x > 100),
  throttleTime(200)
);

// 3. Elaborazione sottoscrizione (esecuzione dei side effect)
const subscription = processed$.subscribe({
  next: x => console.log('Posizione click:', x),
  error: err => console.error(err),
  complete: () => console.log('Completato')
});

Vantaggi

VantaggioDettagli
Significato chiaro per ogni stepLe responsabilità di ogni fase sono evidenti a colpo d'occhio
Facile da debuggareVerificare lo stream intermedio con console.log o tap
Facile da testareTestare stream intermedi come processed$ individualmente
Annidamento ridottoElaborazione semplificata in subscribe
RiutilizzabileL'elaborazione della pipeline può essere estratta come funzione

Variante: separazione delle funzioni (modularizzazione)

Se il processo di trasformazione è lungo, separare la pipeline come funzione.

ts
import { Observable } from 'rxjs';
import { map, filter, distinctUntilChanged } from 'rxjs';
import { fromEvent } from 'rxjs';

// Estrarre l'elaborazione della pipeline come funzione
function transformClicks(source$: Observable<Event>): Observable<number> {
  return source$.pipe(
    map(ev => (ev as MouseEvent).clientX),
    filter(x => x > 100),
    distinctUntilChanged()
  );
}

// Lato utilizzo
const clicks$ = fromEvent(document, 'click');
const xPosition$ = transformClicks(clicks$);
const subscription = xPosition$.subscribe(x => console.log(x));

Punto chiave: Estrarre "come trasformare" come funzione pura aumenta enormemente la testabilità.

Convenzioni di naming

Un naming appropriato chiarisce l'intento del codice.

FaseEsempio di namingSignificato
Sourceclicks$, input$, routeParams$Fonte di eventi e dati
Pipeprocessed$, validInput$, apiResponse$Stream elaborati
Subscriptionsubscription, uiSubscriptionSide effect effettivamente eseguiti

Il suffisso $ indica a colpo d'occhio che si tratta di un Observable.

Per una scrittura più dichiarativa (RxJS 7+)

Estrarre pipe come funzione e renderla riutilizzabile.

ts
import { pipe, fromEvent } from 'rxjs';
import { map, filter } from 'rxjs';

// Definire la pipeline come funzione (riutilizzabile)
const processClicks = pipe(
  map((ev: MouseEvent) => ev.clientX),
  filter(x => x > 100)
);

const clicks$ = fromEvent(document, 'click');
const processed$ = clicks$.pipe(processClicks);
processed$.subscribe(x => console.log(x));

Vantaggio: La logica di elaborazione (processClicks) può essere riutilizzata in altri stream.

Before/After: refactoring per pattern tipici

Esempi di miglioramenti in casi d'uso reali.

A. Evento UI → API → Aggiornamento UI

❌ Before (inferno one-liner)

ts
import { fromEvent } from 'rxjs';
import { throttleTime, switchMap, catchError } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { of } from 'rxjs';

interface ApiRes {
  items: string[];
  error?: string;
}

const button = document.getElementById('btn') as HTMLButtonElement;
const list = document.getElementById('list') as HTMLElement;

fromEvent(button, 'click').pipe(
  throttleTime(500),
  switchMap(() => ajax.getJSON<ApiRes>('/api/items')),
  catchError(err => of({ items: [], error: err.message }))
).subscribe(res => {
  list.innerHTML = res.items.map(item => `<li>${item}</li>`).join('');
  if (res.error) alert(res.error);
});

✅ After (separazione delle fasi + funzionalizzazione)

ts
import { fromEvent, pipe, of } from 'rxjs';
import { throttleTime, switchMap, map, catchError } from 'rxjs';
import { ajax } from 'rxjs/ajax';

interface ApiRes {
  items: string[];
}

interface Result {
  items: string[];
  error: string | null;
}

const button = document.getElementById('btn') as HTMLButtonElement;
const list = document.getElementById('list') as HTMLElement;

// 1) source
const clicks$ = fromEvent(button, 'click');

// 2) pipeline (estratta come funzione pura)
const loadItems = () =>
  pipe(
    throttleTime(500),
    switchMap(() => ajax.getJSON<ApiRes>('/api/items')),
    map((res: ApiRes) => ({ items: res.items, error: null as string | null })),
    catchError(err => of({ items: [] as string[], error: String(err?.message ?? err) }))
  );

const result$ = clicks$.pipe(loadItems());

// 3) subscription (solo side effect)
const subscription = result$.subscribe(({ items, error }) => {
  renderList(items);
  if (error) toast(error);
});

function renderList(items: string[]) {
  list.innerHTML = items.map(item => `<li>${item}</li>`).join('');
}

function toast(message: string) {
  alert(message);
}

Miglioramenti:

  • Elaborazione pipeline loadItems() trasformata in funzione pura
  • Side effect (renderList, toast) consolidati lato subscribe
  • Più facile da testare e debuggare

B. Valori form → validazione → salvataggio API (auto-save)

❌ Before

ts
import { fromEvent } from 'rxjs';
import { map, debounceTime, distinctUntilChanged, filter, switchMap } from 'rxjs';
import { ajax } from 'rxjs/ajax';

const input = document.getElementById('input') as HTMLInputElement;

fromEvent(input, 'input')
  .pipe(
    map((e: Event) => (e.target as HTMLInputElement).value),
    debounceTime(400),
    distinctUntilChanged(),
    filter(v => v.length >= 3),
    switchMap(v => ajax.post('/api/save', { v }))
  )
  .subscribe(
    () => console.log('OK'),
    err => alert(err.message)
  );

✅ After (separazione delle responsabilità + naming)

ts
import { fromEvent, pipe } from 'rxjs';
import { map, debounceTime, distinctUntilChanged, filter, switchMap } from 'rxjs';
import { ajax } from 'rxjs/ajax';

const input = document.getElementById('input') as HTMLInputElement;

// 1) source
const value$ = fromEvent<Event>(input, 'input').pipe(
  map(e => (e.target as HTMLInputElement).value)
);

// 2) pipeline (validazione)
const validate = () =>
  pipe(
    debounceTime(400),
    distinctUntilChanged(),
    filter((v: string) => v.length >= 3)
  );

// 2) pipeline (auto-save)
const autosave = () =>
  pipe(
    switchMap((v: string) => ajax.post('/api/save', { v }))
  );

const save$ = value$.pipe(validate(), autosave());

// 3) subscription
const subscription = save$.subscribe({
  next: () => showSuccess(),
  error: (err) => showError(String(err?.message ?? err))
});

function showSuccess() {
  console.log('Salvato');
}

function showError(message: string) {
  alert(message);
}

Miglioramenti:

  • Separazione di validazione (validate) e salvataggio (autosave)
  • Ogni pipeline ora è riutilizzabile
  • Test più semplici (validazione e salvataggio possono essere testati separatamente)

C. Cache + refresh manuale

ts
import { merge, of, Subject } from 'rxjs';
import { switchMap, shareReplay } from 'rxjs';
import { ajax } from 'rxjs/ajax';

interface Item {
  id: number;
  name: string;
}

const refreshBtn = document.getElementById('refresh-btn') as HTMLButtonElement;

// 1) sources
const refresh$ = new Subject<void>();
const initial$ = of(void 0);

// 2) pipeline
const fetchItems$ = merge(initial$, refresh$).pipe(
  switchMap(() => ajax.getJSON<Item[]>('/api/items')),
  shareReplay({ bufferSize: 1, refCount: true }) // memoizzazione
);

// 3) subscription
const subscription = fetchItems$.subscribe(items => renderList(items));

// Ricarica da UI
refreshBtn?.addEventListener('click', () => refresh$.next());

function renderList(items: Item[]) {
  console.log('Items:', items);
}

Punti chiave:

  • Separare auto-load iniziale (initial$) e refresh manuale (refresh$)
  • Cache dei valori più recenti con shareReplay
  • Più sottoscrittori condividono gli stessi risultati

Avanzato: incorporare log intermedi

Puoi osservare ogni fase con tap().

ts
import { fromEvent } from 'rxjs';
import { map, tap } from 'rxjs';

const clicks$ = fromEvent(document, 'click');

const processed$ = clicks$.pipe(
  tap(() => console.log('Click avvenuto')),
  map(e => (e as MouseEvent).clientX),
  tap(x => console.log('Coordinata X:', x))
);

processed$.subscribe(x => console.log('Valore finale:', x));

Punti chiave:

  • tap è un operatore dedicato ai side effect
  • Puoi osservare il valore di ogni fase durante il debug
  • Dovrebbe essere rimosso in produzione

Dimostrare la testabilità

La separazione delle fasi consente di testare l'elaborazione della pipeline in isolamento.

Esempio: test della validazione input

ts
// validate.ts
import { pipe } from 'rxjs';
import { map, debounceTime, distinctUntilChanged, filter } from 'rxjs';

export const validateQuery = () =>
  pipe(
    map((s: string) => s.trim()),
    debounceTime(300),
    distinctUntilChanged(),
    filter((s) => s.length >= 3)
  );
ts
// validate.spec.ts
import { TestScheduler } from 'rxjs/testing';
import { validateQuery } from './validate';

describe('validateQuery', () => {
  it('trims, debounces, distincts, filters length>=3', () => {
    const scheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });

    scheduler.run(({ hot, expectObservable }) => {
      // Input: " a ", "ab", "abc", "abc ", "abcd"
      const input = hot<string>('-a-b-c--d-e----|', {
        a: ' a ',
        b: 'ab',
        c: 'abc',
        d: 'abc ',
        e: 'abcd'
      });

      const output$ = input.pipe(validateQuery());

      // Atteso: passano solo 'abc' e 'abcd'
      expectObservable(output$).toBe('--------c-----e-|', {
        c: 'abc',
        e: 'abcd'
      });
    });
  });
});

Vantaggi:

  • L'elaborazione della pipeline può essere testata in isolamento
  • Indipendente da DOM/HTTP = veloce e stabile
  • I marble test controllano la timeline

Per maggiori informazioni, vedere Metodologie di test.

Template di istruzioni per GitHub Copilot

Una raccolta di prompt utilizzabili per il refactoring reale.

1. Scomposizione in tre livelli

Refactoring di questo codice RxJS scomponendolo in una struttura a tre livelli "source / pipeline / subscription".
Requisiti:
- Gli Observable sono denominati con suffisso $
- Le pipeline sono estratte come funzioni che restituiscono pipe(...) (es: validate(), loadItems())
- I side effect (aggiornamenti UI, console, toast) sono consolidati in subscribe
- Mettere tap() nei punti appropriati (con commenti) per osservare lo stato intermedio
- I nomi delle variabili e delle funzioni devono comunicare il dominio

2. Chiarire la selezione dell'operatore

Voglio prevenire chiamate API multiple dovute a click multipli.
Suggerisci quale tra switchMap/mergeMap/concatMap/exhaustMap usare,
e sostituisci con l'operatore corretto. Scrivi le motivazioni nei commenti.

Linee guida:
- Il salvataggio form è sequenziale (concatMap)
- I suggerimenti di ricerca scartano le vecchie richieste (switchMap)
- Nessuna doppia esecuzione per click ripetuti (exhaustMap)

3. Pattern auto-save

Refactoring del seguente codice in pattern auto-save:
- Input con debounceTime e distinctUntilChanged
- Salvataggio serializzato con concatMap
- I side effect per notifiche di successo/fallimento all'UI sono lato subscribe
- Funzionalizzare la trasformazione per facilitare i test
- Se possibile, cache dello stato più recente con shareReplay

4. Cache + refresh manuale

Passare al pattern "primo auto-load + refresh manuale":
- Introdurre refresh$ Subject
- merge(initial$, refresh$) → switchMap(fetch)
- Cache dei valori più recenti con shareReplay({bufferSize:1, refCount:true})
- Estrarre la pipe di fetch come funzione per il riutilizzo

Conclusione: sintesi delle linee guida per una scrittura leggibile

VoceContenuto consigliato
✅ 1Observable, pipe e subscribe scritti separatamente
✅ 2Gli stream intermedi indicano il significato con i nomi delle variabili
✅ 3Le pipe complesse sono funzionalizzate
✅ 4tap() permette verifiche intermedie
✅ 5Riutilizzabile con processSomething = pipe(...)
  • L'inferno one-liner è causato dalla commistione di definizione stream, trasformazione e sottoscrizione
  • La sintassi di separazione delle fasi (Source → Pipeline → Subscription) chiarisce le responsabilità
  • Funzionalizzare la pipeline migliora testabilità e riutilizzabilità
  • Il naming appropriato (suffisso $, nomi di variabili significativi) migliora la leggibilità

Sezioni correlate

Prossimi passi

  1. Cercare le aree del codice esistente che sono nell'"inferno one-liner"
  2. Refactoring con la sintassi di separazione delle fasi
  3. Funzionalizzare i processi della pipeline e scrivere unit test
  4. Usare i template di istruzioni Copilot per unificare tutto il team

NOTE

Una trattazione più completa di "Come scrivere RxJS leggibile" sarà affrontata nel futuro Capitolo 13: Pattern pratici.

Pubblicato sotto licenza CC-BY-4.0.