Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

pubsub パターンに対する思いと現時点でのベタープラクティス

なんらかの payload を渡したいとき、インターフェイスを定められない。これに尽きる。静的型付けとも相性が悪い。

payload と言ってるのは要するに window.addEventListener(eventName, event => {}) の event のことだ。TypeScript はかなり頑張って型付けしてくれているが、もし pubsub.addEventListener(event, () => {}) を自作したら型を付けるのがしんどいことは想像に難くない。window.addEventListener もカスタムイベントの event に型をつけるのは面倒だ。

多分ベタープラクティス

ベストではない気がするのでベター。

// 外部にエクスポートしない。
class PubSub {
  private listeners: { [key in PropertyKey]?: ((payload?: any) => void)[] } =
    {};
  addEventListener(eventName: string, callback: (payload?: any) => void) {
    if (!this.listeners[eventName]) {
      this.listeners[eventName] = [];
    }
    this.listeners[eventName]?.push(callback);
  }
  removeEventListener(eventName: string, callback: (payload?: any) => void) {
    this.listeners[eventName] = this.listeners[eventName]?.filter(
      (item) => item !== callback
    );
  }
  dispatchEvent(eventName: string, payload?: any) {
    for (const listener of this.listeners[eventName] || []) {
      listener(payload);
    }
  }
}

const clickEvent = 'BUTTON_CLICK';
const buttonPubsub = new PubSub();
export const buttonObserver = {
  click: () => buttonPubsub.dispatchEvent(clickEvent),
  addClickListener: (callback: () => void) => {
    buttonPubsub.addEventListener(clickEvent, callback);
    return () => buttonPubsub.removeEventListener(clickEvent, callback);
  },
};

このように、buttonOberver を介して PubSub を操作することで、addEventListener と dispatchEvent に対して適切な型をつけられる。さらに、PubSub インスタンスを都度生成すれば、他の Observer とカスタムイベント名が衝突しない。

ただし、PubSub クラスを外部へ露出させない都合上、モジュールへの切り出しが行えないデメリットがある。eslint-plugin-import などでアクセス制限を敷いても良いが、規模感次第だろう。また、テストをする際もステートを都度リセットしなければならず、若干コード量が増える。