🕊️

デザインパターン:Observerパターン

2024/04/08に公開

これは何?

状態変化を通知するパターン

登場人物は以下となる。
🫙サブジェクト  :監視対象となるオブジェクト
🗣️パブリッシャー :サブジェクトを監視し、サブスクライバーに通知する
👀サブスクライバー:パブリッシャーからの通知をトリガーに処理をする

何が嬉しいのか

オブジェクト間の結合を緩くできる
状態変化を効率的に伝達する
コードの拡張性を高める

問題

あるオフィスでは、1秒おきにペットボトルの状態が監視されています。
この緊迫した状況で、空のペットボトルを引き金に今日もまた悲しい事件が起きました。

ある社員のサブスクライバーが怒りをあらわにし、
その他社員のサブスクライバーは怯え発狂したのです。

この悲しき人治主義的なシステムに別れを告げ、
サブスクライバーの生産性と心理的安全性は救われなければなりません。

/* ----------------------------------------
 * 無垢なペットボトル
 ---------------------------------------- */
class PetBottle {
  private _isEmpty = false;

  constructor(private readonly onEmpty: () => void) {}

  get isEmpty(): boolean {
    return this._isEmpty;
  }

  empty(): void {
    this._isEmpty = true;
    this.onEmpty();
  }
}

/* ----------------------------------------
/* 怒りに震えるサブスクライバー
---------------------------------------- */
class AngrySubscriber {
  constructor(private readonly petBottle: PetBottle) {
    this.petBottle.onEmpty = this.onEmpty.bind(this);
  }

  private onEmpty(): void {
    console.log("怒りに震えるサブスクライバー: 見つけた!");
    // ペットボトルを捨てる処理
  }
}

/* ----------------------------------------
/* 怯えるサブスクライバー
---------------------------------------- */
class ScaredSubscriber {
  constructor(private readonly petBottle: PetBottle) {
    this.petBottle.onEmpty = this.onEmpty.bind(this);
  }

  private onEmpty(): void {
    console.log("怯えるサブスクライバー: ああああああああああああああ!!!!!!!!!!!!!!");
    // ペットボトルを捨てる処理
  }
}

/* ----------------------------------------
/* 一連の出来事
---------------------------------------- */
const petBottle = new PetBottle(() => {
  // ペットボトルが空になったときの処理
  console.log("ペットボトルが空になりました");
});

const angrySubscriber = new AngrySubscriber(petBottle);
const scaredSubscriber = new ScaredSubscriber(petBottle);

// サブスクライバーによる状態監視
setInterval(() => {
  if (petBottle.isEmpty) {
    // ペットボトルが空になったときの処理
    console.log("ペットボトルが空になりました");
    petBottle.empty();
  }
}, 1000);

解決

世界はカッティングエッジな技術の産声に耳を傾けます。
ペットボトルはサブジェクトであり、またパブリッシャーになりました。

  • ペットボトルは空になると、自らの状態変化をサブスクライバーへ通知します
    → サブスクライバーはペットボトルの監視業務から解放されます
    → 通知はタスク依頼と認識され、サブスクライバーの心理的安全性が確保されます
/* ----------------------------------------
 * カッティングエッジなペットボトル
 ---------------------------------------- */
class PetBottle {
  private _isEmpty = false;
  private readonly _subscribers: Observer[] = [];

  constructor() {}

  get isEmpty(): boolean {
    return this._isEmpty;
  }

  empty(): void {
    this._isEmpty = true;
    this.notify("ペットボトルが空になりました");
  }

  subscribe(observer: Observer): void {
    this._subscribers.push(observer);
  }

  unsubscribe(observer: Observer): void {
    const index = this._subscribers.indexOf(observer);
    if (index >= 0) {
      this._subscribers.splice(index, 1);
    }
  }

  private notify(message: string): void {
    for (const subscriber of this._subscribers) {
      subscriber.update(message);
    }
  }
}

/* ----------------------------------------
 * 役職としてのサブスクライバー
 ---------------------------------------- */
// 抽象的なサブスクライバー
interface Subscriber {
  update(message: string): void;
}

/* ----------------------------------------
 * 救われたサブスクライバー達
 ---------------------------------------- */
// もう怒りに震えないサブスクライバー
class AngrySubscriber implements Subscriber {
  constructor(private readonly name: string) {}

  update(message: string): void {
    console.log(`${this.name}: 「${message}」ね。そうか、これが静寂か。`);
  }
}

// もう怯えないサブスクライバー
class ScaredSubscriber implements Subscriber {
  constructor(private readonly name: string) {}

  update(message: string): void {
    console.log(`${this.name}: 「${message}」ですって。いっちょ片付けますか!`);
  }
}

/* ----------------------------------------
 * 一連の出来事
 ---------------------------------------- */
const petBottle = new PetBottle();

const angrySubscriber = new AngrySubscriber("もう怒りに震えないサブスクライバー");
const scaredSubscriber = new ScaredSubscriber("もう怯えないサブスクライバー");

petBottle.subscribe(angrySubscriber);
petBottle.subscribe(scaredSubscriber);

petBottle.empty();

その他の推しポイント

  • SNSの通知機能など、利用できる場面が多い
  • 教材はサブジェクトとパブリッシャーを結合しているが、分離することでさらに柔軟性が高くなる
  • パブリッシャーを複数作成し、粒度の異なる情報が提供できる

動く実装例

https://www.typescriptlang.org/play?#code/PQKgBAtF07fwmAoM5DVDIYYZBjDIYoZDPDIA0MgFQwaAdDIFYMgWwwaATDIDsMNg1wwqKtuurBIDGANgIYBnQWAAKAUwAuAIQD2kyb3FgA3ijAAHAE4BLAG79JygPo7BAUQC2GyQE8wAXjAAzfr0HiA3Ou37DyrXF+ABNZADtee2NBAFcAI0FuXTjxLUEALjAAeQTUvVSAbQBdRzBi73VucMFJLRjuSVktAAoASlUAXyR1AHMpMDMrG1s2zLjZWSV+MNV1MDBAyRitGckACzMAOlMLaztvea71cT2R1sy9WR1g2fn59a2dobtS2pivObAHwU2w+R1nCMAETUehMQAyDIAvL0A1gzkQBRDIA-BkA6gyAfQYga0DmAjvNYgkkjoUs1ZLktPktJkch5SalzmBLtdbndvttcYlkqkfhoYoI1kSSWSMepsWAYmFWfjCcSqWSKfyaRcrjc1HcwFUxZIBmFguIAB6vDY-aLxNkEjmbHRa3VZZx86U0zHzAFgZoW7V6gB8TgADO1lSqvgaWcaJWbBBpeDpuOIXZadQAaMAARkFKq6h26818BiMYD+kgBI0s4mE-D6mRqujCPVp9KVn2cTWdapqYHF7K0YFkzgDjzbprSvs+OOD7c2MQ0wX8zSLJb6KbuaaxSCOoEg7HXG5Q4EAnvqAdwDABYMSMAZgyAOwZAK0MgDWGQCdDIB6hkAlwyAEoZAAsMgB+GFgbj8cEBcYDAMCAXqNAEMYwAQt3IS9b0fV8kAtIwtFcKNsjlDs-XHScjGnYtBFLcRy1qC1qwVa5vBXcBPzI+AtzAQBFU0AeIZABiGFFwPvZ8X0AFQT33IziIE4JBfzAQAghkAMQZACQDOFoUAcHTAAkGchABEGJjILfPghBEABBKstFsABlEd+wGawlCLMJJBESk8lSRlmzeBommaLN-AWIJQgiewwn4ItcMratOgzEUJynGcsLLVs8KrGtFUZeZm0mcRNl4WQemaAADAASFRmVcosOkyQAYBlSgLsI6QBYBkAWwZACAGQBdBkEwBpBkAQAZAGUGOjwUATXTACHtKrSsS+cuhXP8hMAegNpLk69mKgpThDATTuH4QJgm0vF2z08MTnEIyTKQizqisxoWjsnNAhCcJIlzNycOCzzfWFVD-Mw7CPPwsKGT9SLqmi2L4qS1L0pOrKwFylR8r6IrAHMGQBNBkAYwYj1KmSwcAQwZAHCGQBxJ0ADblAEUGBEQaqwBAf86zFuu6PjAAA5QBiBJPQAvxUAU3NAGi5HhNs0KQ5AUJRSjCcQAHcxHp+RFGjQUafVMBph6DS5pNFIOycFn2bUoWtJ0sXmiBUSJMkwBohnklj0W8SzWymmaRZDcXczZibdfEWa5dSBWBrV4aFM17oNE5xmYr7QlBeFi2tEFR2ZC5pRNld6NEmms39fbXmfYZ7nNhOYY2k8IA

Discussion