Skip to content

Reactive Programming Reconsidered — L'écart entre philosophie de conception et réalité

La Programmation Réactive (Reactive Programming, ci-après RP) est largement connue comme un paradigme puissant pour le traitement de flux de données asynchrones.

Cependant, la RP est-elle vraiment universelle ? Cette page examine l'écart entre idéal et réalité de la RP, et considère objectivement où elle devrait être utilisée et où elle ne devrait pas l'être.

Idéal vs Réalité de la RP

Idéal : Conception moderne et sophistiquée

La RP est souvent présentée comme suit :

  • Code déclaratif et lisible
  • Peut exprimer le traitement asynchrone de manière concise
  • Peut traiter les flux de données complexes de manière unifiée
  • Technologie centrale de l'architecture réactive

Réalité : Peut réduire la productivité de l'équipe

Cependant, dans les projets réels, les problèmes suivants se produisent :

  • Courbe d'apprentissage très élevée
  • Débogage difficile
  • Tests complexes
  • Baisse de productivité due aux mauvaises utilisations

WARNING

Appliquer la RP à "tout le code" peut augmenter la complexité du code et réduire la productivité de l'équipe.

4 défis que rencontre la RP

1. Hauteur de la courbe d'apprentissage

La maîtrise de la RP nécessite un modèle de pensée différent de la programmation impérative traditionnelle.

Difficulté à suivre le flux de données

typescript
// ❌ Flux de données difficile à voir
source$
  .pipe(
    mergeMap(x => fetchData(x)),
    switchMap(data => processData(data)),
    concatMap(result => saveData(result))
  )
  .subscribe(/*...*/);

Problèmes

  • Différences entre mergeMap, switchMap, concatMap pas intuitives
  • Difficile de suivre où et comment les données sont transformées
  • Difficile d'identifier où l'erreur s'est produite

Difficulté du débogage et des sorties de log

typescript
// Débogage difficile
source$
  .pipe(
    map(x => x * 2),
    filter(x => x > 10),
    mergeMap(x => api(x))
  )
  .subscribe(/*...*/);

// Où l'erreur s'est-elle produite ?
// Dans quel opérateur la valeur a-t-elle disparu ?

TIP

Pour le débogage, utilisez l'opérateur tap(), mais cela représente un coût d'apprentissage supplémentaire.

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

2. Charge cognitive élevée

La RP a plus de 100 opérateurs, avec des choix d'utilisation complexes.

Trop d'options d'opérateurs

BesoinOptionsDifférences
Traiter un tableau séquentiellementconcatMap, mergeMap, switchMap, exhaustMapConcurrence et garantie d'ordre différentes
Combiner plusieurs fluxconcat, merge, combineLatest, zip, forkJoin, raceMéthodes de combinaison différentes
Gestion d'erreurscatchError, retry, retryWhen, onErrorResumeNextStratégies de retry différentes

Faut-il vraiment écrire en RP un traitement qui pourrait se faire avec un simple if ou await ?

typescript
// ❌ Exemple complexifié avec RP
of(user)
  .pipe(
    mergeMap(u => u.isPremium
      ? fetchPremiumData(u)
      : fetchBasicData(u)
    )
  )
  .subscribe(/*...*/);

// ✅ Branchement conditionnel simple
const data = user.isPremium
  ? await fetchPremiumData(user)
  : await fetchBasicData(user);

3. Difficulté des tests

Les tests RP nécessitent la compréhension du contrôle temporel et des Marble Testing.

Coût d'apprentissage du Marble Testing

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

it('Test de 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);
  });
});

Problèmes

  • Nécessité d'apprendre la notation des Marble Diagrams
  • Nécessité de comprendre le mécanisme de contrôle temporel
  • Coût d'apprentissage plus élevé que les tests unitaires normaux

Bugs de synchronisation fréquents

typescript
// ❌ Bug courant : problème de timing d'abonnement
const subject$ = new Subject();

subject$.next(1);  // Cette valeur ne sera pas reçue
subject$.subscribe(x => console.log(x));  // Abonnement tardif
subject$.next(2);  // Celle-ci sera reçue

4. Complexification due aux mauvaises utilisations

Appliquer la RP à tout le code crée une complexité inutile.

Sur-application au CRUD simple

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

// ✅ Promise simple
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('Erreur:', error);
    throw error;
  }
}

IMPORTANT

La RP n'est pas une "balle d'argent" qui résout tous les problèmes. Il est important de discerner les domaines où l'appliquer et ceux à éviter.

Domaines où la RP excelle

La RP n'est pas universelle, mais est très puissante dans les domaines suivants.

1. Traitement de flux de données continus

Optimal pour le traitement de données générées en continu comme les données de capteurs, flux de logs, données temps réel.

typescript
// ✅ Exemple où RP montre ses forces : traitement de données capteur
sensorStream$
  .pipe(
    filter(reading => reading.value > threshold),
    bufferTime(1000),                           // Agrégation toutes les secondes
    map(readings => calculateAverage(readings)),
    distinctUntilChanged()                      // Notifier seulement en cas de changement
  )
  .subscribe(avg => updateDashboard(avg));

2. WebSocket et notifications push

Optimal pour la communication bidirectionnelle et la distribution de données push du serveur.

typescript
// ✅ Traitement réactif de communication WebSocket
const socket$ = webSocket('wss://example.com/socket');

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

3. Systèmes de gestion d'état

Efficace comme base de bibliothèques de gestion d'état comme NgRx, Redux Observable, MobX.

typescript
// ✅ Utilisation de RP dans la gestion d'état (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. I/O non-bloquante backend

Approprié pour le traitement asynchrone backend comme Node.js Streams, Spring WebFlux, Vert.x.

typescript
// ✅ Traitement type RP de 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. Systèmes distribués événementiels

Efficace comme base d'architecture événementielle comme Kafka, RabbitMQ, Akka Streams.

Domaines où la RP est inappropriée

Dans les domaines suivants, ne pas utiliser la RP aboutit à un code plus simple et maintenable.

1. Traitement CRUD simple

Pour les opérations simples de lecture/écriture en base de données, async/await est plus approprié.

typescript
// ❌ Pas besoin d'écrire en RP
getUser(id: string): Observable<User> {
  return this.http.get<User>(`/api/users/${id}`);
}

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

2. Branchements conditionnels simples

Pas besoin de transformer en flux un traitement qui se fait avec un simple if.

typescript
// ❌ Sur-application de RP
of(value)
  .pipe(
    mergeMap(v => v > 10 ? doA(v) : doB(v))
  )
  .subscribe();

// ✅ Branchement conditionnel simple
if (value > 10) {
  doA(value);
} else {
  doB(value);
}

3. Traitement asynchrone ponctuel

Si Promise suffit, pas besoin de transformer en Observable.

typescript
// ❌ Transformation inutile en Observable
from(fetchData()).subscribe(data => process(data));

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

Évolution de la RP : Vers des abstractions plus simples

La philosophie de la RP ne disparaît pas, mais évolue vers des formes plus simples et transparentes.

Angular Signals (Angular 19+)

typescript
// Réactivité basée sur Signals
const count = signal(0);
const doubled = computed(() => count() * 2);

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

count.set(5);  // Simple et intuitif

Caractéristiques :

  • Coût d'apprentissage inférieur à RxJS
  • Débogage facile
  • Réactivité à grain fin

React Concurrent Features

typescript
// Concurrent Rendering de React 18
function UserProfile({ userId }) {
  const user = use(fetchUser(userId));  // Intégration avec Suspense
  return <div>{user.name}</div>;
}

Caractéristiques :

  • Récupération de données déclarative
  • Contrôle automatique des priorités
  • Masque la complexité de RP

Svelte 5 Runes

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

function increment() {
  count++;  // Mise à jour intuitive
}

Caractéristiques :

  • Optimisation par compilateur
  • Pas de boilerplate
  • Transparence de la réactivité

TIP

Ces nouvelles abstractions préservent la valeur centrale de RP (réactivité) tout en réduisant considérablement la complexité.

Politique d'utilisation appropriée de la RP

1. Discerner le domaine du problème

AppropriéInapproprié
Flux de données continusCRUD simple
Communication WebSocketAppel API ponctuel
Intégration d'événements asynchrones multiplesBranchement conditionnel simple
Traitement de données temps réelTransformation de données statiques
Gestion d'étatMise à jour de variable simple

2. Introduction progressive

typescript
// ❌ Ne pas introduire massivement d'un coup
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}`);
  // Tout en Observable
}

// ✅ RPiser seulement les parties nécessaires
class UserService {
  async getUser(id: string): Promise<User> { /* ... */ }
  async updateUser(user: User): Promise<User> { /* ... */ }

  // Observable seulement pour les parties nécessitant mise à jour temps réel
  watchUser(id: string): Observable<User> {
    return this.websocket.watch(`/users/${id}`);
  }
}

3. Considérer le niveau de maîtrise de l'équipe

Situation de l'équipeApproche recommandée
Peu familier avec RPIntroduction limitée (seulement parties avec avantages clairs comme WebSocket)
Certains maîtrisentExpansion progressive (gestion d'état, traitement temps réel)
Tous maîtrisentUtilisation full-stack (frontend à backend)

4. Comparer avec les alternatives

typescript
// Pattern 1 : RP (en cas d'intégration d'événements multiples nécessaire)
combineLatest([
  formValue$,
  validation$,
  apiStatus$
]).pipe(
  map(([value, isValid, status]) => ({
    canSubmit: isValid && status === 'ready',
    value
  }))
);

// Pattern 2 : Signals (réactivité plus simple)
const formValue = signal({});
const validation = signal(false);
const apiStatus = signal('ready');
const canSubmit = computed(() =>
  validation() && apiStatus() === 'ready'
);

// Pattern 3 : async/await (traitement ponctuel)
async function submitForm() {
  const isValid = await validateForm(formValue);
  if (!isValid) return;

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

Résumé

La RP n'est pas universelle

IMPORTANT

La Programmation Réactive n'est ni nuisible ni universelle. C'est un outil spécialisé optimisé pour les problèmes de flux asynchrones et événementiels.

Reconnaître la valeur de RP tout en comprenant ses limites

Domaines où RP excelle

  • Traitement de flux de données continus
  • WebSocket et communication temps réel
  • Systèmes de gestion d'état
  • I/O non-bloquante backend
  • Systèmes distribués événementiels

Domaines où RP est inappropriée

  • Traitement CRUD simple
  • Branchements conditionnels simples
  • Traitement asynchrone ponctuel

Transition vers de nouvelles abstractions

La philosophie de RP évolue vers des formes plus simples et transparentes comme Angular Signals, React Concurrent Features, Svelte Runes.

Directives d'application pratique

  1. Discerner le domaine du problème - La RP est-elle vraiment nécessaire ?
  2. Introduction progressive - Ne pas tout adopter d'un coup
  3. Considérer le niveau de maîtrise de l'équipe - Le coût d'apprentissage est élevé
  4. Comparer avec les alternatives - async/await ou Signals suffisent-ils ?

TIP

"Utiliser l'outil approprié, au bon endroit" C'est la clé du succès avec RP.

Pages connexes

Références

Publié sous licence CC-BY-4.0.