Angular: ドメインロジックをシグナル化する(カウンター編)

2024/12/03に公開

これはAngularアドベントカレンダー 3日目の記事です。昨日はaerealさんでした。

https://qiita.com/advent-calendar/2024/angular

しっかりしたのは25日に向けて書くので、今回は軽めのネタです。Signalをそのまま使うのではなく、ある特定のルールを持った値を「シグナル化」する試みを出していこうと思います。第一弾として、いわゆるカウンターをシグナル化してみます。

Signalized counter

というわけで完成形です。

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

export type CounterSignal = Signal<number> & {
  increment: () => void;
  decrement: () => void;
  reset: () => void;
  asReadonly: () => Signal<number>;
};

export function counterSignal(initialValue = 0): CounterSignal {
  const counter = signal(initialValue);
  return Object.assign(counter.asReadonly(), {
    increment: () => counter.update((v) => v + 1),
    decrement: () => counter.update((v) => v - 1),
    reset: () => counter.set(initialValue),
    asReadonly: () => counter.asReadonly(),
  });
}

CounterSignalはシグナルとして読み取り用のインターフェースを備えつつ、書き込みについては専用のメソッドだけが公開されており、通常のWritableSignalにあるset()update()は持っていません。これにより、カウンターとしてのルールに従った方法でのみ値が更新されうるシグナルオブジェクトを作ることができます。asReadonly()は書き込み操作を不能にするためのお作法です。

実際に使うとこんな感じです。自身の更新ロジックを内包しているので利用側はメソッドを呼ぶだけです。

import { counterSignal } from './signalized';

@Component({
  selector: 'app-root',
  template: `
    <p>{{ counter() }}</p>
    <div>
      <button (click)="counter.increment()">++</button>
      <button (click)="counter.decrement()">--</button>
      <button (click)="counter.reset()">RESET</button>
    </div>
  `,
})
export class App {
  readonly counter = counterSignal();
}

こんな感じで、組み込みのシグナルAPIを直接使うだけでなく、ビジネスロジックやドメインモデルを「シグナル化」するというアプローチはけっこう面白いと思っているので、思いついたらまた記事を書きます。

実際に動作するサンプルコードはこちらです。

ありがとうございました。Angularアドベントカレンダー、明日はtakataroさんです。

https://qiita.com/advent-calendar/2024/angular

Discussion