この記事は?
オライリー社から出版されている「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側の実装だけで済む
- WeatherDataの計測対象項目が増えたら?(例えば
風速
とか)- interfaceにより変更対象が明確化
-
update
メソッドの変更が容易?- interfaceで変更が制限される
まとめ
Observerパターンの特徴
- InterfaceでSubject, Observerの定義
- observersはSubjectで管理し、登録、削除、通知を行う
- Observerでは通知を受け取る為の共通メソッド
update
を持たせる
Subject, Observerの関係性を整理しているのと、interfaceで制限しているのが特徴的ですね。
色々使えそうで便利です。