Dev Study
← 解説「constructor注入と BehaviorSubject — 旧世代のサービスの読み方」に戻る

サンプルコードで身につける: constructor注入と BehaviorSubject — 旧世代のサービスの読み方

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1constructor注入の省略記法を分解する

constructor(private http: HttpClient)という1行は、実は3つの処理の省略形です。分解した形と並べると、なぜthis.httpが使えるのかが明確になります。

// この省略記法は…
export class UserService {
  constructor(private http: HttpClient) {}
}

// …これと同じ意味(プロパティ宣言+受け取り+代入)
export class UserService2 {
  private http: HttpClient;      // 1. プロパティ宣言

  constructor(http: HttpClient) { // 2. DIが引数に渡してくる
    this.http = http;             // 3. 代入
  }
}

// 現代版の対訳:
//   private http = inject(HttpClient);
// 読み方: コンストラクタの引数リスト = このクラスの依存一覧

2BehaviorSubject と signal の対訳表

旧ストアと新ストアは概念が1対1で対応します。この対訳を持ってレガシーコードを読むと、「これはシグナルで言うとsetか」と即座に翻訳できるようになります。

// ── BehaviorSubject ⇔ signal 対訳表 ──
//
// 旧(RxJS)                          新(シグナル)
// new BehaviorSubject(初期値)         signal(初期値)
// subject.next(値)                    sig.set(値)
// subject.value                       sig()
// subject.asObservable()              sig.asReadonly()
// obs$.pipe(map(...))  ※派生値       computed(() => ...)
// combineLatest([a$, b$])             computed(() => [a(), b()])
// テンプレートで {{ x$ | async }}      {{ x() }}
//
// どちらも設計は同じ:
//   「書き込みは private に閉じ、読み取り専用で公開する」

3同じストアを新旧で書き比べる

カートストアをBehaviorSubject版とsignal版で並べます。行ごとに対応しているので、旧版を読んで新版に翻訳する練習にそのまま使えます。

// ── 旧: BehaviorSubject 版 ──
@Injectable({ providedIn: 'root' })
export class CartService {
  private itemsSubject = new BehaviorSubject<string[]>([]);
  items$ = this.itemsSubject.asObservable();
  count$ = this.items$.pipe(map((i) => i.length));
  add(item: string) {
    this.itemsSubject.next([...this.itemsSubject.value, item]);
  }
}

// ── 新: signal 版(1行ずつ対応)──
@Injectable({ providedIn: 'root' })
export class CartStore {
  private itemsSignal = signal<string[]>([]);
  items = this.itemsSignal.asReadonly();
  count = computed(() => this.items().length);
  add(item: string) {
    this.itemsSignal.update((items) => [...items, item]);
  }
}

4toSignal で新旧の境界をつなぐ

既存のBehaviorSubjectストアを書き換えずに、新しく作るシグナル中心のコンポーネントから使う方法です。境界にtoSignalを1枚挟むだけで、旧ストアがそのまま新世界の部品になります。

import { Component, inject, computed } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { CartService } from './cart.service'; // 旧ストアのまま

@Component({
  selector: 'app-cart-badge',
  template: '<span>{{ label() }}</span>',
})
export class CartBadgeComponent {
  private cart = inject(CartService);

  // 境界で Observable → シグナルに変換
  count = toSignal(this.cart.count$, { initialValue: 0 });

  // ここから先は普通のシグナルの世界
  label = computed(() =>
    this.count() === 0 ? 'カートは空' : `${this.count()} 件`,
  );
}
// 旧ストア側の書き換えはゼロ。移行はこの形で少しずつ進める

5状態の流れを追うときは .next() を検索する

レガシーコードのデバッグで「この値はどこで変わっている?」を調べる実践手順です。書き込み経路が.next()に限られるストア設計なら、検索するだけで変更箇所を全部列挙できます。

# 「カートの中身が意図せず変わる」を調査する手順

# 1. まず状態の持ち主(Subject)を探す
grep -rn "BehaviorSubject" src/app --include="*.ts"
#   → cart.service.ts: itemsSubject が見つかる

# 2. その Subject への書き込み箇所を全部出す
grep -rn "itemsSubject.next" src/app
#   → add() と clear() の2箇所だけ、と分かる

# 3. あとは2箇所にブレークポイントを置いて再現操作する
#   (デバッグのセクションで学んだ手順そのまま)

# シグナル世代でも同じ: .set( と .update( を検索すれば
# 書き込み経路を全列挙できる。public な subject / signal に
# 誰でも書ける設計だと、この調査が破綻する(だから private)