3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Typescriptでデザインパターンを考えてみる(Observer編)

Last updated at Posted at 2023-05-12

この記事は?

オライリー社から出版されている「HeadFirst デザインパターン」を読んだ内容を
理解の整理のためTypescriptで実装してみる内容です。(定期で載せれる様がんばる)

Observerパターン

背景設定

  • 気象センサーから気象情報を収集し、出力する機能が存在する(WeatherData
  • WeatherData.measurementsChanged()メソッドは、新しい気象観測データを取得するたびに呼び出される

目的

  • 新しく気象情報を表示する為のデバイス(Display)に、WeatherDataが収集した気象情報をリアルタイムに反映させる機能を作る

既存コード

index.ts
// 気象情報管理オブジェクト
class WeatherData {
  temperature: number; // 気温
  humidity: number; // 湿度
  pressure: number; // 気圧

  constructor() {
    this.temperature = 0;
    this.humidity = 0;
    this.pressure = 0;
  }

  // 気象情報を更新する。センサーから値の変更があった際に、呼び出される。
  setMeasurements(temp: number, humi: number, pres: number): void {
    this.temperature = temp;
    this.humidity = humi;
    this.pressure = pres;
    this.measurementsChanged();
  }

  // 値に変更があった場合に呼び出される
  measurementsChanged() {}
}

// 新しいデバイスオブジェクト
class Display {
  temperature: number; // 気温
  humidity: number; // 湿度
  pressure: number; // 気圧

  constructor() {
    this.temperature = 0;
    this.humidity = 0;
    this.pressure = 0;
  }

  // 値を表示する
  display(): void {
    console.log("temp: ", this.temperature);
    console.log("humi: ", this.humidity);
    console.log("pres: ", this.pressure);
  }
}

やりたいこと

index.ts
const wd = new WeatherData()
wd.setMeasurements(1, 2, 3)

const dp = new Display()
dp.display() // => 値 1, 2, 3 が表示される

ぱっとやりそうなNGケース

index.ts
class WeatherData {
  temperature: number;
  humidity: number;
  pressure: number;
  display: Display; // displayをpropertyとして持てるようにする

  constructor(display: Display) {
    this.temperature = 0;
    this.humidity = 0;
    this.pressure = 0;
    this.display = display; // 初期化の際にDisplayを受け取る
  }

  setMeasurements(temp: number, humi: number, pres: number): void {
    this.temperature = temp;
    this.humidity = humi;
    this.pressure = pres;
    this.measurementsChanged();
  }

  measurementsChanged() {
    // コールバック時に、値を直接displayに反映させる
    const temp = this.temperature;
    const humi = this.humidity;
    const pres = this.pressure;
    this.display.update(temp, humi, pres);
  }
}

class Display {
  temperature: number;
  humidity: number;
  pressure: number;

  constructor() {
    this.temperature = 0;
    this.humidity = 0;
    this.pressure = 0;
  }

  display(): void {
    console.log("temp: ", this.temperature);
    console.log("humi: ", this.humidity);
    console.log("pres: ", this.pressure);
  }

  // 値を反映できるメソッドを用意
  update(temperature: number, humidity: number, pressure: number): void {
    this.temperature = temperature;
    this.humidity = humidity;
    this.pressure = pressure;
  }
}

実際に動作させてみる

index.ts
const dp = new Display();
const wd = new WeatherData(dp);

wd.setMeasurements(1, 2, 3);
dp.display();
console.log
temp:  
1
humi:  
2
pres:  
3

実際に動作はします。
では何がダメなんでしょう?

ダメな理由を考えてみる(重要)

  • Display以外のデバイスが出てきたら?
    • コンストラクターに追加していく?100個あったら? 最悪ですね。
  • WeatherDataの計測対象項目が増えたら?(例えば風速 とか)
    • measurementsChangedにハードコードされてる通知メソッドと、displayのupdateを書き直す必要がある。
    • 通知対象のデバイスがもっとおおくなったら? 100個とかあったら? 地獄ですね。
  • updateメソッドの変更が容易?
    • Display側からupdateメソッドの変更に伴う影響が見えない?
    • 変えるとどこに影響がある? WeatherDataが壊れますね。 障害発生です。
    • 依存関係が WeatherData > Display とあるべきが、いつのまにか Display > WeatherDataになってる

などなど。

Observerパターン使用後

まずはinterfaceを定義してみる。

interface.ts
// 通知機能を持つオブジェクトのinterfaceを定義
export interface Subject {
  observers: Observer[];
  registerObserver(o: Observer): void;
  removeObserver(o: Observer): void;
  notifyObservers(): void;
}

// 通知の受信機能を持つオブジェクトのinterfaceを定義
export interface Observer {
  update(temperature: number, humidity: number, pressure: number): void;
}
index.ts
import { Subject, Observer } from './interface'

class WeatherData implements Subject {
  temperature: number;
  humidity: number;
  pressure: number;
  observers: Observer[]; // observersをSubject側で管理できるようにする

  constructor() {
    this.temperature = 1;
    this.humidity = 2;
    this.pressure = 3;
    this.observers = []; // observersを初期化
  }

  // observerを追加
  registerObserver(o: Observer): void {
    this.observers = [...this.observers, o];
  }

  // observerを削除
  removeObserver(o: Observer): void {
    this.observers = this.observers.filter((v) => v !== o);
  }

  // observersに通知する
  notifyObservers(): void {
    this.observers.forEach((o) => {
      o.update(this.temperature, this.humidity, this.pressure);
    });
  }

  setMeasurements(temp: number, humi: number, pres: number): void {
    this.temperature = temp;
    this.humidity = humi;
    this.pressure = pres;
    this.measurementsChanged();
  }

  // 値に変更があった場合に呼び出される
  measurementsChanged() {
    this.notifyObservers();
  }
}

class Display implements Observer {
  temperature: number;
  humidity: number;
  pressure: number;

  constructor(w: WeatherData) {
    this.temperature = 0;
    this.humidity = 0;
    this.pressure = 0;
    w.registerObserver(this); // Observerとして自信をSubjectに登録する
  }

  display(): void {
    console.log("temp: ", this.temperature);
    console.log("humi: ", this.humidity);
    console.log("pres: ", this.pressure);
  }

  // Observerとして値を受信できるメソッドを定義
  update(temperature: number, humidity: number, pressure: number): void {
    this.temperature = temperature;
    this.humidity = humidity;
    this.pressure = pressure;
  }
}

実行してみる。

index.ts
const wd = new WeatherData();
const dp = new Display(wd);

wd.setMeasurements(1, 2, 3);
dp.display();
console.log
temp:  
1
humi:  
2
pres:  
3

良い感じ。さっきの課題を再検討してみます。

  • Display以外のデバイスが出てきたら?
    • 新規observer側の実装だけで済む :thumbsup:
  • WeatherDataの計測対象項目が増えたら?(例えば風速 とか)
    • interfaceにより変更対象が明確化 :tada:
  • updateメソッドの変更が容易?
    • interfaceで変更が制限される :clap:

まとめ

Observerパターンの特徴

  • InterfaceでSubject, Observerの定義
  • observersはSubjectで管理し、登録、削除、通知を行う
  • Observerでは通知を受け取る為の共通メソッドupdateを持たせる

Subject, Observerの関係性を整理しているのと、interfaceで制限しているのが特徴的ですね。
色々使えそうで便利です。 :thumbsup:

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?