Dev Study
← 解説「サービスとDI — ロジックの置き場所」に戻る

サンプルコードで身につける: サービスとDI — ロジックの置き場所

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

1最小のサービスを定義する

サービスの最小例で、@Injectable を付けたただのクラスです。console.log を画面のあちこちに直書きせず、ログの出し方をこの1箇所に集約できます。「画面に関係しないロジックの置き場所」というサービスの役割が最も素朴に表れた形です。

import { Injectable } from '@angular/core';

// @Injectable を付けると、DIが管理できるクラスになる
@Injectable({ providedIn: 'root' })
export class LoggerService {
  log(message: string) {
    console.log('[app]', message);
  }
}

// ログの形式を変えたくなっても、修正はこのクラスだけで済む

2計算ロジックをサービスに切り出す

税込価格や割引の計算のように、複数の画面で使う計算ルールはサービスにまとめます。コンポーネントごとに同じ計算式をコピーすると、税率変更のときに直し漏れが起きるからです。「ビジネスルールはサービス、表示はコンポーネント」という実務の分担を示す例です。

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class PriceService {
  private taxRate = 0.1;

  withTax(price: number) {
    return Math.floor(price * (1 + this.taxRate));
  }

  discounted(price: number, rate: number) {
    return Math.floor(price * (1 - rate));
  }
}

// 税率が変わっても、修正するのはこのサービスだけ

3画面をまたいでデータを共有する

providedIn: 'root' のサービスはアプリ全体で1つだけ作られるため、コンポーネントをまたいだデータの置き場所になります。商品ページで add した閲覧履歴を、トップページで list して表示する、という画面間の連携がこの仕組みで実現できます。シングルトンという性質が実務でどう効くかが分かる例です。

import { Injectable } from '@angular/core';

// providedIn: 'root' なのでアプリ全体で1つだけ作られる
@Injectable({ providedIn: 'root' })
export class RecentlyViewedService {
  private productIds: number[] = [];

  add(productId: number) {
    // 先頭に追加し、直近5件だけを残す
    this.productIds = [productId, ...this.productIds].slice(0, 5);
  }

  list() {
    return this.productIds;
  }
}

// 商品ページで add し、トップページで list する、という共有ができる

4シグナルを持つ状態管理サービス

サービスの中でシグナルと computed を持たせると、アプリ全体で共有されるリアクティブな状態置き場になります。どの画面から add しても、件数を表示しているすべての場所が自動で更新されます。モダンAngularの状態管理は、まずこの「シグナル入りサービス」から始めるのが定石です。

import { Injectable, computed, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class CartStateService {
  private items = signal<string[]>([]);
  // 外には読み取り専用の派生値として公開する
  readonly count = computed(() => this.items().length);

  add(itemName: string) {
    this.items.update((list) => [...list, itemName]);
  }
}

// どの画面から add しても、count を表示している場所がすべて更新される

5お気に入り管理サービスを設計する

「追加済みなら外し、未追加なら入れる」というトグル動作を持つ、お気に入り機能の実務的なサービスです。判定(has)と更新(toggle)をサービスに集約しているので、お気に入りボタンを置くどの画面も同じルールで動きます。ECサイトやブックマーク機能の縮図といえる設計です。

import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class FavoriteService {
  private ids = signal<number[]>([]);

  toggle(productId: number) {
    // 追加済みなら外し、未追加なら入れる
    this.ids.update((current) =>
      current.includes(productId)
        ? current.filter((id) => id !== productId)
        : [...current, productId],
    );
  }

  has(productId: number) {
    return this.ids().includes(productId);
  }
}