← 解説「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)