Skip to content

Reactive Programming Reconsidered — Kloof tussen ontwerpfilosofie en realiteit

Reactive Programming (hierna RP) is algemeen bekend als een krachtig paradigma voor asynchrone datastream verwerking.

Maar is RP echt universeel? Deze pagina verifieert de kloof tussen ideaal en realiteit van RP en overweegt objectief waar RP moet worden gebruikt en waar niet.

Ideaal vs Realiteit van RP

Ideaal: Verfijnd modern ontwerp

RP wordt vaak als volgt gepromoot:

  • Declaratieve en leesbare code
  • Kan asynchrone verwerking beknopt uitdrukken
  • Kan complexe dataflows op uniforme manier behandelen
  • Kerntechnologie van Reactieve Architectuur

Realiteit: Kan ook teamproductiviteit verlagen

In werkelijke projecten doen zich echter de volgende problemen voor:

  • Zeer hoge leercurve
  • Debugging is moeilijk
  • Testing is complex
  • Productiviteitsverlies door verkeerd gebruik

WARNING

Het toepassen van RP op "alle code" kan juist de complexiteit van code verhogen en teamproductiviteit verlagen.

4 Uitdagingen van RP

1. Hoge leercurve

Het beheersen van RP vereist een ander denkmodel dan traditioneel imperatief programmeren.

Moeilijk te traceren dataflow

typescript
// ❌ Datastroom is moeilijk te zien
source$
  .pipe(
    mergeMap(x => fetchData(x)),
    switchMap(data => processData(data)),
    concatMap(result => saveData(result))
  )
  .subscribe(/*...*/);

Problemen

  • Verschil tussen mergeMap, switchMap, concatMap is niet intuïtief
  • Moeilijk te traceren waar en hoe data wordt getransformeerd
  • Moeilijk te identificeren waar fout is opgetreden

Moeilijkheid van debugging en logging

typescript
// Debugging is moeilijk
source$
  .pipe(
    map(x => x * 2),
    filter(x => x > 10),
    mergeMap(x => api(x))
  )
  .subscribe(/*...*/);

// Waar is de fout opgetreden?
// Bij welke operator is de waarde verdwenen?

TIP

Voor debugging gebruik je de tap() operator, maar dit is zelf een extra leercost.

typescript
source$
  .pipe(
    tap(x => console.log('voor map:', x)),
    map(x => x * 2),
    tap(x => console.log('na map:', x)),
    filter(x => x > 10),
    tap(x => console.log('na filter:', x))
  )
  .subscribe(/*...*/);

2. Hoge cognitieve belasting

RP heeft meer dan 100 operators en onderscheid maken is complex.

Te veel operator keuzes

VereisteKeuzesVerschil
Array sequentieel verwerkenconcatMap, mergeMap, switchMap, exhaustMapGelijktijdigheid en volgorde garantie verschillen
Combineren van meerdere streamsconcat, merge, combineLatest, zip, forkJoin, raceCombinatiemethode verschilt
Error handlingcatchError, retry, retryWhen, onErrorResumeNextRetry strategie verschilt

Is het echt nodig om verwerking die met simpele if of await kan worden gedaan met RP te schrijven?

typescript
// ❌ Complex geschreven voorbeeld met RP
of(user)
  .pipe(
    mergeMap(u => u.isPremium
      ? fetchPremiumData(u)
      : fetchBasicData(u)
    )
  )
  .subscribe(/*...*/);

// ✅ Simpele conditional
const data = user.isPremium
  ? await fetchPremiumData(user)
  : await fetchBasicData(user);

3. Moeilijkheid van testen

Testen van RP vereist begrip van tijdscontrole en Marble Testing.

Leercost van Marble Testing

typescript
import { TestScheduler } from 'rxjs/testing';

it('test van debounceTime', () => {
  const testScheduler = new TestScheduler((actual, expected) => {
    expect(actual).toEqual(expected);
  });

  testScheduler.run(({ cold, expectObservable }) => {
    const input$  = cold('-a-b-c---|');
    const expected =     '-----c---|';

    const result$ = input$.pipe(debounceTime(50, testScheduler));

    expectObservable(result$).toBe(expected);
  });
});

Problemen

  • Moet Marble Diagram notatie leren
  • Moet mechanisme van tijdscontrole begrijpen
  • Hogere leercost dan normale unit tests

Frequente synchronisatie bugs

typescript
// ❌ Veelvoorkomende bug: subscription timing probleem
const subject$ = new Subject();

subject$.next(1);  // Deze waarde kan niet worden ontvangen
subject$.subscribe(x => console.log(x));  // Subscription is te laat
subject$.next(2);  // Dit kan wel worden ontvangen

4. Complexiteit door verkeerd gebruik

Het toepassen van RP op alle code creëert onnodige complexiteit.

Overmatige toepassing op simpele CRUD verwerking

typescript
// ❌ Overmatige RP toepassing
getUserById(userId: string): Observable<User> {
  return this.http.get<User>(`/api/users/${userId}`)
    .pipe(
      map(user => this.transformUser(user)),
      catchError(error => {
        console.error('Fout:', error);
        return throwError(() => error);
      })
    );
}

// ✅ Simpele Promise
async getUserById(userId: string): Promise<User> {
  try {
    const user = await fetch(`/api/users/${userId}`).then(r => r.json());
    return this.transformUser(user);
  } catch (error) {
    console.error('Fout:', error);
    throw error;
  }
}

IMPORTANT

RP is geen "silver bullet" die alle problemen oplost. Het is belangrijk om te bepalen welke gebieden wel en niet geschikt zijn voor toepassing.

Gebieden waar RP uitblinkt

RP is niet universeel, maar is zeer krachtig in de volgende gebieden.

1. Continue datastream verwerking

Optimaal voor verwerking van continu optredende data zoals sensordata, log streams, realtime data.

typescript
// ✅ Voorbeeld waar RP zijn sterkte toont: sensordata verwerking
sensorStream$
  .pipe(
    filter(reading => reading.value > threshold),
    bufferTime(1000),                           // Aggregeer per seconde
    map(readings => calculateAverage(readings)),
    distinctUntilChanged()                      // Alleen notificeren bij verandering
  )
  .subscribe(avg => updateDashboard(avg));

2. WebSocket en push notificaties

Optimaal voor bidirectionele communicatie en server push-type data distributie.

typescript
// ✅ Reactieve verwerking van WebSocket communicatie
const socket$ = webSocket('wss://example.com/socket');

socket$
  .pipe(
    retry({ count: 3, delay: 1000 }),  // Automatische herverbinding
    map(msg => parseMessage(msg)),
    filter(msg => msg.type === 'notification')
  )
  .subscribe(notification => showNotification(notification));

3. State management systemen

Effectief als basis voor state management libraries zoals NgRx, Redux Observable, MobX.

typescript
// ✅ RP toepassing in state management (NgRx Effects)
loadUsers$ = createEffect(() =>
  this.actions$.pipe(
    ofType(UserActions.loadUsers),
    mergeMap(() =>
      this.userService.getUsers().pipe(
        map(users => UserActions.loadUsersSuccess({ users })),
        catchError(error => of(UserActions.loadUsersFailure({ error })))
      )
    )
  )
);

4. Backend non-blocking I/O

Geschikt voor asynchrone verwerking in backend zoals Node.js Streams, Spring WebFlux, Vert.x.

typescript
// ✅ RP-achtige verwerking van Node.js Streams
const fileStream = fs.createReadStream('large-file.txt');
const transformStream = new Transform({
  transform(chunk, encoding, callback) {
    const processed = processChunk(chunk);
    callback(null, processed);
  }
});

fileStream.pipe(transformStream).pipe(outputStream);

5. Event-driven gedistribueerde systemen

Effectief als basis voor event-driven architectuur zoals Kafka, RabbitMQ, Akka Streams.

Gebieden waar RP niet geschikt is

In de volgende gebieden levert niet gebruiken van RP simpelere en beter onderhoudbare code op.

1. Simpele CRUD verwerking

Voor simpele lees-schrijf operaties naar database is async/await geschikter.

typescript
// ❌ Niet nodig om met RP te schrijven
getUser(id: string): Observable<User> {
  return this.http.get<User>(`/api/users/${id}`);
}

// ✅ async/await is voldoende
async getUser(id: string): Promise<User> {
  return await fetch(`/api/users/${id}`).then(r => r.json());
}

2. Simpele conditional

Voor verwerking die met simpele if statement kan is het niet nodig om stream te maken.

typescript
// ❌ Overmatige RP toepassing
of(value)
  .pipe(
    mergeMap(v => v > 10 ? doA(v) : doB(v))
  )
  .subscribe();

// ✅ Simpele conditional
if (value > 10) {
  doA(value);
} else {
  doB(value);
}

3. Eenmalige asynchrone verwerking

Wanneer Promise voldoende is, is het niet nodig om Observable te maken.

typescript
// ❌ Onnodige Observable conversie
from(fetchData()).subscribe(data => process(data));

// ✅ Promise is voldoende
fetchData().then(data => process(data));

RP evolutie: Naar eenvoudigere abstractie

De filosofie van RP verdwijnt niet, maar evolueert naar eenvoudigere en transparantere vorm.

Angular Signals (Angular 19+)

typescript
// Signal-gebaseerde reactiviteit
const count = signal(0);
const doubled = computed(() => count() * 2);

effect(() => {
  console.log('Count:', count());
});

count.set(5);  // Simpel en intuïtief

Kenmerken:

  • Lagere leercost dan RxJS
  • Debugging is gemakkelijk
  • Fijnmazige reactiviteit

React Concurrent Features

typescript
// React 18's Concurrent Rendering
function UserProfile({ userId }) {
  const user = use(fetchUser(userId));  // Geïntegreerd met Suspense
  return <div>{user.name}</div>;
}

Kenmerken:

  • Declaratieve data fetching
  • Automatische prioriteitscontrole
  • Verbergt complexiteit van RP

Svelte 5 Runes

typescript
// Svelte 5's Runes ($state, $derived)
let count = $state(0);
let doubled = $derived(count * 2);

function increment() {
  count++;  // Intuïtieve update
}

Kenmerken:

  • Compiler optimalisatie
  • Geen boilerplate
  • Transparantie van reactiviteit

TIP

Deze nieuwe abstracties behouden de kernwaarde van RP (reactiviteit) terwijl ze complexiteit drastisch verminderen.

Juiste gebruiksrichtlijnen voor RP

1. Bepaal probleemgebied

GeschiktNiet geschikt
Continue datastreamsSimpele CRUD
WebSocket communicatieEenmalige API call
Integratie van meerdere asynchrone gebeurtenissenSimpele conditional
Realtime data verwerkingStatische data transformatie
State managementSimpele variabele update

2. Gefaseerde introductie

typescript
// ❌ Niet direct volledig introduceren
class UserService {
  getUser$ = (id: string) => this.http.get<User>(`/api/users/${id}`);
  updateUser$ = (user: User) => this.http.put<User>(`/api/users/${user.id}`, user);
  deleteUser$ = (id: string) => this.http.delete(`/api/users/${id}`);
  // Alles Observable maken
}

// ✅ Alleen noodzakelijke delen RP maken
class UserService {
  async getUser(id: string): Promise<User> { /* ... */ }
  async updateUser(user: User): Promise<User> { /* ... */ }

  // Alleen delen die realtime updates nodig hebben Observable
  watchUser(id: string): Observable<User> {
    return this.websocket.watch(`/users/${id}`);
  }
}

3. Houd rekening met team bekwaamheid

Team situatieAanbevolen aanpak
Onbekend met RPBeperkte introductie (alleen delen met duidelijke voordelen zoals WebSocket)
Deel bekwaamGeleidelijke uitbreiding (state management, realtime verwerking)
Iedereen bekwaamFullstack gebruik (frontend tot backend)

4. Vergelijk met alternatieven

typescript
// Patroon 1: RP (wanneer integratie van meerdere gebeurtenissen nodig is)
combineLatest([
  formValue$,
  validation$,
  apiStatus$
]).pipe(
  map(([value, isValid, status]) => ({
    canSubmit: isValid && status === 'ready',
    value
  }))
);

// Patroon 2: Signals (eenvoudigere reactiviteit)
const formValue = signal({});
const validation = signal(false);
const apiStatus = signal('ready');
const canSubmit = computed(() =>
  validation() && apiStatus() === 'ready'
);

// Patroon 3: async/await (eenmalige verwerking)
async function submitForm() {
  const isValid = await validateForm(formValue);
  if (!isValid) return;

  const result = await submitToApi(formValue);
  return result;
}

Samenvatting

RP is niet universeel

IMPORTANT

Reactive Programming is noch schadelijk noch universeel. Het is een gespecialiseerde tool geoptimaliseerd voor asynchrone en event flow problemen.

Erken waarde van RP terwijl beperkingen worden begrepen

Gebieden waar RP uitblinkt

  • Continue datastream verwerking
  • WebSocket en realtime communicatie
  • State management systemen
  • Backend non-blocking I/O
  • Event-driven gedistribueerde systemen

Gebieden waar RP niet geschikt is

  • Simpele CRUD verwerking
  • Simpele conditional
  • Eenmalige asynchrone verwerking

Migratie naar nieuwe abstracties

De filosofie van RP evolueert naar eenvoudigere en transparantere vormen zoals Angular Signals, React Concurrent Features, Svelte Runes.

Richtlijnen voor praktische toepassing

  1. Bepaal probleemgebied - Is RP echt nodig?
  2. Gefaseerde introductie - Niet direct volledig adopteren
  3. Houd rekening met team bekwaamheid - Leercost is hoog
  4. Vergelijk met alternatieven - Zijn async/await of Signals voldoende?

TIP

"Gebruik de juiste tool op de juiste plaats" Dit is de sleutel tot succes met RP.

Gerelateerde pagina's

Referenties

Uitgebracht onder de CC-BY-4.0 licentie.