ソフトウェア設計

Swift Pub-Subパターンとオブザーバーパターンの違い(イベントバス総まとめ)

iOSアプリを作っていると、いつか一度はおなじみの壁にぶつかります。

読了 5 分
Swift Pub-Subパターンとオブザーバーパターンの違い(イベントバス総まとめ)のカバー画像

iOSアプリを作っていると、いつか一度はおなじみの壁にぶつかります。

画面Aで起きたことを遠く離れた画面Bに知らせたい。でも、どうつなげればいいのでしょうか。

デリゲートを次々に渡していると、コードはスパゲッティのように絡まりがちです。そんなときに使えるのがSwift Pub-Subパターンとイベントバスです。

今回は、Swift Pub-Subパターンとオブザーバーパターンの違い、そしてイベントバスとは何かを整理します。

結論から言うと、オブザーバーパターンは「主体が購読者を直接知る」構造で、Pub-Subは間に仲介役(イベントバス)を置いて互いを知らないようにします。

この一文を理解すれば、あとはすんなり入ってきます。コードと一緒に順に見ていきましょう。


まずはオブザーバーパターンから

オブザーバーパターンは、「観察対象(Subject)」と「オブザーバー(Observer)」を直接つなぐ構造です。

対象に変化が起きると、登録済みのオブザーバーへ直接通知を送ります。

iOSを使ったことがあれば、すでに利用しているはずです。代表例はNotificationCenter、Combineの@Published、KVO(Key-Value Observing)です。

ポイントは、観察対象が自分の購読者リストを直接保持していることです。

// 観察対象が購読者を直接管理
class Subject {
    private var observers: [Observer] = []
    func add(_ o: Observer) { observers.append(o) }
    func notify() { observers.forEach { $0.update() } }
}

構造がシンプルで直感的なので、1対Nの関係に向いています。

ただし、規模が大きくなると、対象がオブザーバーの存在を把握しておくことが負担になります。


Swift Pub-Subパターンは何が違う?

Pub-Subパターン(発行・購読パターン)は、ここからさらに一歩進んだものです。

発行者(Publisher)と購読者(Subscriber)の間に仲介役を置きます。この仲介役がイベントバス、またはメッセージブローカーです。

発行者はバスに「こんなことが起きた」と投げるだけです。誰が受け取るかは知りません。

購読者もバスに「このイベントが来たら知らせて」と登録するだけです。誰が送ったかは知りません。

互いをまったく知らないことがポイントです。これを疎結合(decoupling)と呼びます。

直接接続と、バスを介した接続を図で見るとこうなります
直接接続と、バスを介した接続を図で見るとこうなります
// イベントバスが発行者と購読者の間を仲介
enum AppEvent { case userLoggedIn(id: String) }

final class EventBus {
    static let shared = EventBus()
    private var handlers: [(AppEvent) -> Void] = []
    func subscribe(_ h: @escaping (AppEvent) -> Void) { handlers.append(h) }
    func publish(_ e: AppEvent) { handlers.forEach { $0(e) } }
}

発行者はEventBus.shared.publish(.userLoggedIn(id: "123"))の1行で終わりです。

ログイン画面はホーム画面の存在すら知る必要がありません。

EventBusの1行で発行完了。こんなにシンプルです
EventBusの1行で発行完了。こんなにシンプルです

オブザーバー vs Pub-Subを表で比較

言葉だけでは似て見えるので、違いを表にまとめました。

項目 オブザーバーパターン Pub-Subパターン
仲介役 なし(直接接続) あり(イベントバス)
結合度 対象が購読者を知っている 互いに知らない(疎結合)
関係 主に1:N N:Nも可能
iOSの例 KVO, @Published NotificationCenter、イベントバス
適した場面 特定オブジェクトの状態監視 離れたモジュール間の通信

面白いことに、NotificationCenterは実際にはPub-Subに近い存在です。

名前は「Notification」ですが、発行者と購読者はNotificationCenterというバスを通じてのみ出会います。互いを直接参照しません。

そのため「オブザーバーパターン=NotificationCenter」と覚えると少しずれます。概念的にはPub-Subに近いものです。


では、いつ何を使うべき?

実務で使っている判断基準を紹介します。

1つのオブジェクトの状態変化を近い範囲で監視するなら、オブザーバー系が便利です。Combineの@PublishedやSwiftUIの@Observableがまさに適しています。

一方、離れたモジュール間やアプリ全体で起きるログイン、決済完了、ネットワーク切断などの大きなイベントを伝えるなら、イベントバスのほうがずっとすっきりします。

ただし、イベントバスも万能ではありません。

多用すると「このイベントはいったいどこから来たの?」と流れを追いにくくなります。疎結合を得る代わりに、見通しを少し失うわけです。

そこで私は、画面内のロジックにはオブザーバー(Combine)を使い、画面やモジュールをまたぐ大きなイベントだけをイベントバスで扱っています。

大きなイベントだけをバスに集めると、流れがすっきりします
大きなイベントだけをバスに集めると、流れがすっきりします

Q. Combineを使えばイベントバスは不要ですか?

いいえ。CombineのPassthroughSubjectだけで、簡単なイベントバスを自作できます。道具が重なるだけで、概念が置き換わるわけではありません。

Q. Pub-Subは常に優れたパターンですか?

そうとは限りません。疎結合にする必要のない近い関係なら、オブザーバーのほうが読みやすくデバッグもしやすいです。


オブザーバーパターンは直接接続、Pub-Subパターンは仲介役を介した接続。違いはこれだけです。

両者は競合するものではなく、状況に応じて選ぶ道具です。次に画面を1つ作るときは、両者が互いをどれだけ知る必要があるかを考えてみてください。どのパターンが合うか、すぐ見えてくるはずです。

あわせて読みたい