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

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

https://www.oreilly.co.jp/books/9784873119762/

# 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以外のデバイスが出てきたら？
    - コンストラクターに追加していく？１００個あったら？　最悪ですね。
- 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: