Skip to content

Reactive Programming Reconsidered — 設計哲学と現実のギャップ

Reactive Programming(リアクティブプログラミング、以下RP)は、非同期データストリーム処理の強力なパラダイムとして広く知られています。

しかし、RPは本当に万能なのでしょうか? このページでは、RPの理想と現実のギャップを検証し、どこでRPを使うべきか、どこで使うべきでないかを客観的に考察します。

RPの理想 vs 現実

理想:洗練されたモダン設計

RPは以下のように宣伝されることが多いです。

  • 宣言的で読みやすいコード
  • 非同期処理を簡潔に表現できる
  • 複雑なデータフローを統一的に扱える
  • リアクティブアーキテクチャの中核技術

現実:チームの生産性を下げることもある

しかし、実際のプロジェクトでは以下のような問題が発生しています。

  • 学習曲線が非常に高い
  • デバッグが困難
  • テストが複雑
  • 誤用による生産性低下

WARNING

RPを「すべてのコード」に適用すると、逆にコードの複雑さが増し、チームの生産性が低下する可能性があります。

RPが抱える4つの課題

1. 学習曲線の高さ

RPの習得には、従来の命令型プログラミングとは異なる思考モデルが必要です。

データフローの追跡が困難

typescript
// ❌ データの流れが見えにくい
source$
  .pipe(
    mergeMap(x => fetchData(x)),
    switchMap(data => processData(data)),
    concatMap(result => saveData(result))
  )
  .subscribe(/*...*/);

問題点

  • mergeMap, switchMap, concatMap の違いが直感的でない
  • データがどこでどう変換されているか追跡しにくい
  • エラーがどこで発生したのか特定が困難

デバッグとログ出力の難しさ

typescript
// デバッグが困難
source$
  .pipe(
    map(x => x * 2),
    filter(x => x > 10),
    mergeMap(x => api(x))
  )
  .subscribe(/*...*/);

// どこでエラーが起きたのか?
// どのオペレーターで値が消えたのか?

TIP

デバッグには tap() オペレーターを使いますが、これ自体が追加の学習コストです。

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

2. 認知負荷の高さ

RPには100以上のオペレーターがあり、使い分けが複雑です。

オペレーターの選択肢が多すぎる

要件選択肢違い
配列を順次処理concatMap, mergeMap, switchMap, exhaustMap並行度と順序保証が異なる
複数ストリームの結合concat, merge, combineLatest, zip, forkJoin, race結合方法が異なる
エラーハンドリングcatchError, retry, retryWhen, onErrorResumeNextリトライ戦略が異なる

シンプルなifawaitで済む処理を、わざわざRPで書く必要があるのか?

typescript
// ❌ RPで複雑に書いた例
of(user)
  .pipe(
    mergeMap(u => u.isPremium
      ? fetchPremiumData(u)
      : fetchBasicData(u)
    )
  )
  .subscribe(/*...*/);

// ✅ シンプルな条件分岐
const data = user.isPremium
  ? await fetchPremiumData(user)
  : await fetchBasicData(user);

3. テストの難しさ

RPのテストには、時間制御とMarble Testing(マーブルテスト)の理解が必要です。

Marble Testingの学習コスト

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

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

問題点

  • Marble Diagramの記法を学ぶ必要がある
  • 時間制御の仕組みを理解する必要がある
  • 通常のユニットテストより学習コストが高い

同期バグの頻発

typescript
// ❌ よくあるバグ:購読タイミングの問題
const subject$ = new Subject();

subject$.next(1);  // この値は受け取れない
subject$.subscribe(x => console.log(x));  // 購読が遅い
subject$.next(2);  // これは受け取れる

4. 誤用による複雑化

RPをすべてのコードに適用すると、不必要な複雑さが生まれます。

単純なCRUD処理への過剰適用

typescript
// ❌ 過剰なRP適用
getUserById(userId: string): Observable<User> {
  return this.http.get<User>(`/api/users/${userId}`)
    .pipe(
      map(user => this.transformUser(user)),
      catchError(error => {
        console.error('エラー:', error);
        return throwError(() => error);
      })
    );
}

// ✅ シンプルな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('エラー:', error);
    throw error;
  }
}

IMPORTANT

RPは「すべての問題を解決する銀の弾丸」ではありません。 適用すべき領域と、避けるべき領域を見極めることが重要です。

RPが優れている領域

RPは万能ではありませんが、以下の領域では非常に強力です。

1. 連続的なデータストリーム処理

センサーデータ、ログストリーム、リアルタイムデータなど、連続的に発生するデータの処理に最適です。

typescript
// ✅ RPが強みを発揮する例:センサーデータの処理
sensorStream$
  .pipe(
    filter(reading => reading.value > threshold),
    bufferTime(1000),                           // 1秒ごとに集約
    map(readings => calculateAverage(readings)),
    distinctUntilChanged()                      // 変化があったときだけ通知
  )
  .subscribe(avg => updateDashboard(avg));

2. WebSocketとプッシュ通知

双方向通信やサーバーからのプッシュ型データ配信に最適です。

typescript
// ✅ WebSocket通信のリアクティブ処理
const socket$ = webSocket('wss://example.com/socket');

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

3. 状態管理システム

NgRx、Redux Observable、MobXなど、状態管理ライブラリの基盤として有効です。

typescript
// ✅ 状態管理でのRP活用(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

Node.js Streams、Spring WebFlux、Vert.xなど、バックエンドの非同期処理に適しています。

typescript
// ✅ Node.js StreamsのRP的処理
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. イベント駆動分散システム

Kafka、RabbitMQ、Akka Streamsなど、イベント駆動アーキテクチャの基盤として有効です。

RPが不向きな領域

以下の領域では、RPを使わない方がシンプルで保守性が高いコードになります。

1. 単純なCRUD処理

データベースへの単純な読み書き操作には、async/await の方が適しています。

typescript
// ❌ RPで書く必要はない
getUser(id: string): Observable<User> {
  return this.http.get<User>(`/api/users/${id}`);
}

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

2. シンプルな条件分岐

単純なif文で済む処理を、わざわざストリームにする必要はありません。

typescript
// ❌ 過剰なRP適用
of(value)
  .pipe(
    mergeMap(v => v > 10 ? doA(v) : doB(v))
  )
  .subscribe();

// ✅ シンプルな条件分岐
if (value > 10) {
  doA(value);
} else {
  doB(value);
}

3. 一度きりの非同期処理

Promiseで十分な場合は、Observableにする必要はありません。

typescript
// ❌ 不要なObservable化
from(fetchData()).subscribe(data => process(data));

// ✅ Promiseで十分
fetchData().then(data => process(data));

RPの進化:よりシンプルな抽象化へ

RPの哲学は消えつつあるのではなく、よりシンプルで透明な形に進化しています。

Angular Signals(Angular 19+)

typescript
// Signalベースのリアクティビティ
const count = signal(0);
const doubled = computed(() => count() * 2);

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

count.set(5);  // シンプルで直感的

特徴:

  • RxJSより学習コストが低い
  • デバッグが容易
  • 細粒度のリアクティビティ

React Concurrent Features

typescript
// React 18のConcurrent Rendering
function UserProfile({ userId }) {
  const user = use(fetchUser(userId));  // Suspenseと統合
  return <div>{user.name}</div>;
}

特徴:

  • 宣言的なデータフェッチ
  • 自動的な優先順位制御
  • RPの複雑さを隠蔽

Svelte 5 Runes

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

function increment() {
  count++;  // 直感的な更新
}

特徴:

  • コンパイラによる最適化
  • ボイラープレートなし
  • リアクティビティの透明性

TIP

これらの新しい抽象化は、RPの核心的な価値(リアクティビティ) を保ちながら、複雑さを大幅に削減しています。

RPの適切な活用方針

1. 問題領域を見極める

適している適していない
連続データストリーム単純なCRUD
WebSocket通信一度きりのAPI呼び出し
複数の非同期イベント統合シンプルな条件分岐
リアルタイムデータ処理静的なデータ変換
状態管理単純な変数更新

2. 段階的に導入する

typescript
// ❌ いきなり全面導入しない
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}`);
  // すべてObservable化
}

// ✅ 必要な部分だけRP化
class UserService {
  async getUser(id: string): Promise<User> { /* ... */ }
  async updateUser(user: User): Promise<User> { /* ... */ }

  // リアルタイム更新が必要な部分だけObservable
  watchUser(id: string): Observable<User> {
    return this.websocket.watch(`/users/${id}`);
  }
}

3. チームの習熟度を考慮する

チームの状況推奨アプローチ
RPに不慣れ限定的に導入(WebSocketなど明確な利点がある部分のみ)
一部が習熟段階的拡大(状態管理、リアルタイム処理)
全員が習熟フルスタックで活用(フロントエンド〜バックエンド)

4. 代替手段と比較する

typescript
// パターン1: RP(複数イベントの統合が必要な場合)
combineLatest([
  formValue$,
  validation$,
  apiStatus$
]).pipe(
  map(([value, isValid, status]) => ({
    canSubmit: isValid && status === 'ready',
    value
  }))
);

// パターン2: Signals(よりシンプルなリアクティビティ)
const formValue = signal({});
const validation = signal(false);
const apiStatus = signal('ready');
const canSubmit = computed(() =>
  validation() && apiStatus() === 'ready'
);

// パターン3: async/await(一度きりの処理)
async function submitForm() {
  const isValid = await validateForm(formValue);
  if (!isValid) return;

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

まとめ

RPは万能ではない

IMPORTANT

Reactive Programmingは有害でも万能でもありません。非同期およびイベントフローの問題に最適化された専門ツールです。

RPの価値を認めつつ、限界を理解する

RPが優れている領域

  • 連続データストリーム処理
  • WebSocketとリアルタイム通信
  • 状態管理システム
  • バックエンドのノンブロッキングI/O
  • イベント駆動分散システム

RPが不向きな領域

  • 単純なCRUD処理
  • シンプルな条件分岐
  • 一度きりの非同期処理

新しい抽象化への移行

RPの哲学は、Angular Signals、React Concurrent Features、Svelte Runesのようなよりシンプルで透明な形に進化しています。

実務での適用指針

  1. 問題領域を見極める - RPが本当に必要か?
  2. 段階的に導入する - いきなり全面採用しない
  3. チームの習熟度を考慮する - 学習コストは高い
  4. 代替手段と比較する - async/awaitやSignalsで十分か?

TIP

「適切なツールを、適切な場所で使う」 これがRPを成功させる鍵です。

関連ページ

参考資料

Released under the CC-BY-4.0 license.