Featured image of post コード探偵ロックの事件簿【Publish-Subscribe】掲示板に貼れ〜Observerが越えられなかった壁〜

コード探偵ロックの事件簿【Publish-Subscribe】掲示板に貼れ〜Observerが越えられなかった壁〜

分散環境でObserverの直接呼び出しが引き起こす障害連鎖を、Publish-Subscribeパターンで解消する方法をPerl/Mooで解説。通知パターンの進化系譜を辿るアーク総括回。

事件の発端 — 3本の鎖

カフェで待っていた。

オフィス街の、昼休みにはサラリーマンで埋まるチェーン店。午後2時を過ぎて人がまばらになった窓際の席で、アイスコーヒーのグラスに水滴がつくのを眺めながら、ノートを広げていた。

A5のノートの左ページに、手描きの図がある。OrderService から3本の矢印が InventoryServiceShippingServiceBillingService に伸びている。各矢印の横に notify() と書き添えた。Observerパターンの構成図。半年前にモノリスから分割したときの設計図を、今朝の通勤電車で清書したものだ。

3ヶ月前までは、この図の通りに動いていた。OrderService が Subject、3つのサービスが Observer。注文が確定したら、順番に notify() を呼ぶ。シンプルだった。

——マイクロサービスに分割するまでは。

先月だけで3回、請求書サービスの障害で注文確定が止まった。先週は45分間。上長の「Observerの設計を見直せ」という声がまだ耳に残っている。見直すって、何を。Observer は間違っていないはず。リスナーを登録して、イベントが起きたら通知する。教科書通りだ。

教科書通りのはずなのに、壊れた。

技術カンファレンスの懇親会で、SREの佐藤さんが教えてくれた名前を思い出した。「変なコード探偵がいるらしいよ。レガシーコード専門で、キーボードで報酬を取るんだって」。半信半疑で検索したら、古くさいHTMLの1ページだけのウェブサイトが出てきた。レガシー・コード・インベスティゲーション。LCI。問い合わせフォームから連絡したら、翌日に返信が来た。「場所と日時を指定したまえ——L」。

だからカフェを指定した。事務所に行くのは——佐藤さんの説明だと「変な人」らしいので——初対面はカフェがいい。何かあればすぐ出られるし。

ドアが開いた。

ツイードのジャケットを着た男が入ってきた。年齢不詳。周囲を一瞥して、迷いなくこちらに向かってくる。手には何も持っていない。鞄もPCもない。ジャケットの内ポケットだけが微かに膨らんでいる。

立ち上がりかけて、声をかけた。

「あの、ロックさんですか? メールでやり取りさせていただいた——」

座りながら、わたしのノートに目を落とした。

「……ほう」

「え?」

「この矢印は何本ある?」

ノートの図を指差している。自己紹介もなく、挨拶もなく、わたしの名前も聞かず。

「3本ですけど——」

「3本。Subject から Observer への直接参照が3本。——3本の鎖だ、ワトソン君」

面食らった。

「……わたしの名前は——」

聞いていない。店員にエスプレッソを頼んでいる。

佐藤さんが言ってた「変な人」ってこういうことか。……まあいい。技術の話ができればそれでいい。

「3本の鎖、というのは?」

「Observer パターン。Subject が Observer のリストを保持し、状態が変わったら順番に notify() を呼ぶ。——君の図の通りだ。Subject は Observer を知っている。Observer のインターフェースへの参照を直接持っている。これが鎖だ」

「鎖というより、配線では。イベントの配線です」

ロックさんが少し目を細めた。

「配線でもいい。配線は、同じ部屋の中で壁のコンセントに挿すものだ。君のシステムは、かつては同じ部屋にあった。モノリスだった。コンセントも壁も、すべて同じプロセスの中にあった」

「はい。半年前までは1つのPerlアプリケーションでした」

「そして3ヶ月前にマイクロサービスに分割した。部屋を壁で仕切った。在庫サービスは隣の部屋、配送サービスはその隣、請求書サービスはさらにその向こう——」

「……はい」

「壁で仕切った後も、君は同じコンセントに配線を挿そうとしている。壁を突き破って」

ノートの図を見下ろした。3本の矢印。notify()。HTTP越しの同期呼び出し。

「……HTTPに変えました。Observer の notify() を HTTP の API 呼び出しに」

「HTTPに変えた。だが構造は変わっていない。OrderService は3つのサービスのエンドポイントを知っている。呼び出したらレスポンスが返るまで待つ。Observer の Subject が Observer を直接呼ぶ構造そのものだ。プロトコルが変わっただけで、鎖は切れていない

先週の45分間の障害を思い出した。請求書サービスのDBメンテナンス中、OrderService が notify() のHTTPタイムアウトで止まった。3つのサービスのうち1つが落ちただけで、全体が止まった。

「……Observer を使えばいいと思っていたんです。リスナーを追加するだけで拡張できるし、疎結合のはず——」

「疎結合?」

ノートの図を指された。OrderService からの3本の矢印。

「Subject は Observer のインターフェースに依存している。インターフェースだから実装には依存していない——それが Observer の"疎結合"だ。しかし、参照そのものは持っている。呼び出しは同期的だ。Observer が応答しなければ Subject は待つ。Observer がクラッシュすれば Subject のスレッドが巻き込まれる。——これを疎結合と呼ぶなら、疎結合の定義を君と僕とで話し合う必要がある」

唇を引き結んだ。反論したかった。でも45分の障害が反論を封じている。

現場検証 — 壁に穴を開けて腕を突っ込む

ノートPCを開いて、コードを見せた。

「これが今のコードです」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package OrderService;
use Moo;
use Types::Standard qw(ArrayRef);

has observers => (
    is      => 'ro',
    isa     => ArrayRef,
    default => sub { [] },
);

sub add_observer ($self, $observer) {
    push $self->observers->@*, $observer;
}

sub confirm_order ($self, $order) {
    $order->{status} = 'confirmed';

    # Observer に通知(HTTP越しの同期呼び出し)
    for my $observer ($self->observers->@*) {
        $observer->notify($order);  # ← ここがHTTPタイムアウトで詰まる
    }

    return $order;
}

ロックさんはエスプレッソを受け取りながら、画面を覗き込んだ。

$observer->notify($order)。このループが3回回る。3つのサービスを順番に、同期的に呼んでいる」

「はい。最初に在庫引当、次に配送手配、最後に請求書発行です」

「請求書発行が落ちたとき、何が起きる?」

notify がHTTPタイムアウトで例外を投げます。confirm_order 全体が失敗します。在庫引当と配送手配は——成功したものもあれば、していないものもあります」

「成功したものは巻き戻せるかね?」

「……正直、そこまで考えていませんでした」

エスプレッソを一口。カップをソーサーに戻して。

「整理しよう。問題は3つある」

ロックさんがわたしのノートの空きスペースに、許可を取らずにペンを走らせ始めた。内ポケットから万年筆を出している。レトロな趣味だ。少し驚いたけれど、止めなかった。

1
2
3
問題1: 順次同期呼び出し → 1つの障害で全体停止
問題2: Subject が全 Observer を直接知っている → 追加に Subject 周辺の変更が必要
問題3: 通知の成否が Subject に跳ね返る → 責務の混在

「3番目が本質だ。OrderService の仕事は注文を確定することであって、通知を届けることではない。しかし今の設計では、通知の失敗が注文の確定を巻き戻す。——配達員の風邪で手紙が書けなくなる。おかしいだろう?」

ノートの余白に書かれた3行を読んだ。問題3が刺さった。OrderService が通知の責任まで負っているから、通知の失敗で注文が死ぬ。

「……でも、Observer パターンは Subject と Observer を分離するためのパターンですよね? インターフェースで抽象化して——」

Observer パターンは同じ部屋で正しく動くパターンだ。同一プロセス、同一メモリ空間。Subject が Observer のメソッドを直接呼べる距離にいる。——君のシステムは壁で仕切られた。別プロセス、別サーバー、別ネットワーク。Observer が設計された時代に、この壁は想定されていなかった

「HTTP に変えたのは——」

「壁に穴を開けて腕を突っ込んだだけだ。穴の向こうで相手が腕を掴んでいる間、君は動けない。タイムアウトの45分間、OrderService は穴の向こうの請求書サービスが腕を離すのを待っていた。——Observer は壁を越えられない。Observer は壁がないことを前提としたパターンだ

推理披露 — 掲示板に貼れ

ロックさんが万年筆を手に取り、わたしのノートの右ページに図を描き始めた。左ページにはわたしが描いたObserverの構成図。右ページは白紙だった。

「声が届かない相手に、どう伝える?」

「……メールとか?」

「メール——つまり、相手の宛先を知っていて、直接送る。それはObserverのHTTP版と何が違う?」

考え込んだ。直接送るのがダメなら——。

掲示板に貼れ

「掲示板?」

ノートの右ページに四角い枠を描いて、枠の上に「Topic」と書いた。

「通知を送る代わりに、掲示板に貼り紙を出す。“注文が確定した"という貼り紙を。誰が読むかは知らない。何人読むかも知らない。掲示板に貼るだけだ」

「誰が読むかわからないのに、それで大丈夫なんですか?」

「読む側が掲示板を見に来る。在庫引当は『注文確定の貼り紙が出たら在庫を引き当てる』というルールで掲示板を監視する。配送も、請求書も同じだ。——貼る人と読む人が、互いを知らない。これが Publish-Subscribe パターンだ」

ノートの右ページに図を描き足していく。

1
2
3
[OrderService] --publish--> [Topic: order.confirmed] --subscribe--> [InventoryService]
                                                      --subscribe--> [ShippingService]
                                                      --subscribe--> [BillingService]

左ページのObserver図と、右ページのPub/Sub図。並べて見ると、違いが一目でわかった。

「矢印の形が……変わりましたね。左のページではOrderServiceから3本の矢印が直接出ている。右のページではOrderServiceからTopicに1本、Topicから3つのサービスに3本」

「左のページでは、OrderServiceが3つのサービスの存在を知っている。右のページでは、OrderServiceはTopicの存在だけを知っている。3つのサービスはTopicの存在だけを知っている。OrderServiceと3つのサービスの間に、直接の参照がなくなった

「Observerの Subject は Observer を知っていた。Publisher は Subscriber を知らない……」

「その通り。Observer は声で呼ぶ——相手の顔が見える距離で。Pub/Sub は掲示板に貼る——誰が読むかは掲示板が決める

掲示板の管理人——MessageBroker

「掲示板の管理人は、誰がやるんですか?」

メッセージブローカーだ。トピックを管理し、メッセージを受け取り、購読者に配信する。Observerの Subject が担っていた『通知の配信』という責務を、ブローカーが引き受ける」

ロックさんがわたしのPCに手を伸ばした。「借りるよ」と一言。エディタにコードを打ち始める。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package MessageBroker;
use Moo;
use Types::Standard qw(HashRef ArrayRef);

has _subscriptions => (
    is      => 'ro',
    isa     => HashRef[ArrayRef],
    default => sub { {} },
);

sub subscribe ($self, $topic, $handler) {
    push $self->_subscriptions->{$topic}->@*, $handler;
}

sub publish ($self, $topic, $message) {
    my $handlers = $self->_subscriptions->{$topic} // [];
    for my $handler ($handlers->@*) {
        eval { $handler->handle($message) };
        if ($@) {
            warn "Handler failed for topic '$topic': $@";
            # ハンドラの失敗はPublisherに伝播しない
        }
    }
}

eval で囲んでいる……ハンドラが失敗しても、publish は止まらない」

「Observer では notify() の例外が Subject に直撃した。Pub/Sub では、ブローカーがハンドラの障害を吸収する。請求書サービスが落ちても、在庫引当と配送手配には影響しない。そしてOrderServiceにも影響しない」

「——45分間の障害が、起きなくなる?」

「正確に言えば、起きてもOrderServiceが止まらなくなる。請求書サービスのDBメンテナンスはまだ起きる。だが、注文の確定は続く。請求書は、復旧後に追いつけばいい」

Publisher と Subscriber のロール

「Publisher と Subscriber をロールとして定義する」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package Role::Publisher;
use Moo::Role;
use Types::Standard qw(InstanceOf);

has broker => (
    is       => 'ro',
    isa      => InstanceOf['MessageBroker'],
    required => 1,
);

sub publish_event ($self, $topic, $message) {
    $self->broker->publish($topic, $message);
}
1
2
3
package Role::Subscriber;
use Moo::Role;
requires 'handle';

Role::Subscriberhandle を要求するだけ……」

「Subscriber が何をするかはブローカーの知るところではない。Subscriber は handle メソッドを実装し、ブローカーに登録するだけだ。ブローカーは Subscriber の実装を知らない。Subscriber はブローカーの実装を知らない。知っているのはトピック名だけだ」

OrderService を書き直す

「では、OrderService を書き直そう」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package OrderService;
use Moo;
with 'Role::Publisher';

sub confirm_order ($self, $order) {
    $order->{status} = 'confirmed';

    # トピックにメッセージを発行(誰が読むかは知らない)
    $self->publish_event('order.confirmed', {
        order_id => $order->{id},
        items    => $order->{items},
        total    => $order->{total},
    });

    return $order;
}

左ページのBeforeコードを確認した。add_observer がなくなっている。observers のリストもない。ループもない。

add_observer が消えた。observers のリストもない」

「OrderServiceはもうObserverを知らない。Observerという概念すら持っていない。やることは1つ——Topicに貼り紙を出す。以上だ」

「配線を挿す代わりに、掲示板に貼っただけ……」

「配線を引き抜いたのではない。配線そのものが不要になった。OrderServiceと3つのサービスの間に、直接の接続はない。ブローカーが間に立っている」

Subscriber の実装

「Subscriber側を1つ見せよう。在庫引当の例だ」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package InventorySubscriber;
use Moo;
with 'Role::Subscriber';

has inventory_service => (is => 'ro', required => 1);

sub handle ($self, $message) {
    $self->inventory_service->allocate(
        $message->{order_id},
        $message->{items},
    );
}

「シンプルですね……Observerのときと違って、OrderServiceへの参照がない」

「Observerの Observer は、Subject を知っている——最低でも、Subject の型を知っている。Subscriber はPublisherを知らない。Topicを知っているだけだ」

「トピック名……order.confirmed。これだけがPublisherとSubscriberをつなげている」

文字列1つだ。オブジェクトへの参照ではない。インターフェースへの依存でもない。文字列の一致——これがPub/Subの結合度だ」

全体の組み立て

「最後に、全体の配線をブローカーに委ねる」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 初期化(アプリケーションのブートストラップ)
my $broker = MessageBroker->new;

# Subscriber をトピックに登録
$broker->subscribe('order.confirmed', InventorySubscriber->new(
    inventory_service => $inventory_service,
));
$broker->subscribe('order.confirmed', ShippingSubscriber->new(
    shipping_service => $shipping_service,
));
$broker->subscribe('order.confirmed', BillingSubscriber->new(
    billing_service => $billing_service,
));

# Publisher にブローカーを渡す
my $order_service = OrderService->new(broker => $broker);

右ページに、自分の手でPublisher → Topic → Subscriberの構成を描き直していた。

「4つ目のサービスを追加したいとき、OrderServiceのコードを変更する必要があるかね?」

「……ない。ブローカーに新しいSubscriberを登録するだけ」

「Observerなら?」

「Observerを追加する。Subject の add_observer を呼ぶ。——あ、でもSubjectのコードは変わらない」

「Subject のコードは変わらない。しかしSubject が Observer を知っている事実は変わらないadd_observer を呼ぶ箇所はアプリケーションのどこかにある。そこでは Subject と Observer の両方の型が必要だ。Pub/Sub では、Subscriber の登録に Publisher の型は不要だ。PublisherとSubscriberが同じファイルに現れることすらない

	classDiagram
    class MessageBroker {
        -_subscriptions: HashRef
        +subscribe(topic, handler)
        +publish(topic, message)
    }

    class `Role::Publisher` {
        +broker: MessageBroker
        +publish_event(topic, message)
    }

    class `Role::Subscriber` {
        +handle(message)*
    }

    class OrderService {
        +confirm_order(order)
    }

    class InventorySubscriber {
        +handle(message)
    }

    class ShippingSubscriber {
        +handle(message)
    }

    class BillingSubscriber {
        +handle(message)
    }

    OrderService ..|> `Role::Publisher` : with
    InventorySubscriber ..|> `Role::Subscriber` : with
    ShippingSubscriber ..|> `Role::Subscriber` : with
    BillingSubscriber ..|> `Role::Subscriber` : with
    `Role::Publisher` --> MessageBroker : broker
    MessageBroker --> `Role::Subscriber` : dispatch

事件の解決 — 消失と重複

ロックさんがエスプレッソの2杯目を頼んだ。わたしはPCの画面にテストコードを表示した。

「テストを書いてみたんですけど……」

「テストは君が書くべきだ、ワトソン君。自分のシステムを理解するのは君だ」

少し驚いた。でも、その通りだ。

核心のテストは1つ。請求書サービスが障害でも注文が確定するかどうか。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
subtest 'billing failure does not block order confirmation' => sub {
    my $broker = MessageBroker->new;
    my $order_service = OrderService->new(broker => $broker);

    my @inventory_calls;
    $broker->subscribe('order.confirmed', MockSubscriber->new(
        on_handle => sub ($msg) { push @inventory_calls, $msg },
    ));

    # 障害を起こすSubscriber(請求書サービス)
    $broker->subscribe('order.confirmed', FailingSubscriber->new);

    my $order = $order_service->confirm_order({
        id => 'ORD-001', items => ['item-A'], total => 1500,
    });

    is $order->{status}, 'confirmed', '注文は確定する';
    is scalar @inventory_calls, 1, '在庫引当は実行される';
};

「請求書サービスが die しても、注文は confirmed になって、在庫引当は実行される」

「Observerなら、請求書サービスの dieconfirm_order を巻き戻していた。Pub/Subでは、ブローカーが例外を吸収する。Publisherは Subscriber の成否を知らない。知る必要がない」

「でも——請求書が発行されないままになりませんか?」

「なる。Subscriberが復旧するまで、請求書は未発行だ。——だが、注文は確定している。在庫は引き当てられている。配送は手配されている。ビジネスは止まっていない。請求書は復旧後にリトライすればいい」

「Observerでは全部止まっていた」

「Observerでは、1つのサービスの障害が全体を人質に取った。Pub/Subでは、障害は局所に閉じ込められる

ふと、疑問がわいた。

「Subscriberがダウンしていたら、メッセージは消えるのですか? Observerでは notify の瞬間に相手がいなかったら——声は消えましたよね」

「Observerではそうだ。notify の瞬間に Observer が不在なら、通知は空気に溶ける。——Pub/Subでは、掲示板に貼り紙が残る。Subscriberが復旧したとき、貼り紙を読めばいい。メッセージブローカーは、未処理のメッセージを保持する」

「最低1回は届く……at-least-once delivery」

「知っているのか」

「言葉だけは。実装では使ったことがなくて」

「at-least-once。最低1回は届く。しかし、ちょうど1回とは限らない。ブローカーの確認応答が遅れれば、同じメッセージが再送される」

「2回届く可能性がある?」

「ある」

眉をひそめた。

「思い出したまえ。Observerでは届かなかった。Pub/Subでは重複する可能性がある。届かないメッセージは復元できない。重複したメッセージは、冪等な処理で1つを捨てればいい。——消失と重複、どちらが対処しやすい?

しばらく黙った。左ページのObserver図を見ていた。3本の矢印。45分間、声が届かなかった3本の鎖。

「……重複のほうが、ずっとマシです」

「問題が消えたのではない。問題の性質が変わった。対処不可能な問題が、対処可能な問題に変わった」

「メッセージIDで重複を弾けばいい、ということですね」

「その通りだ」

通知の歴史 — 声が届く範囲を広げる

ロックさんが立ち上がる気配がした。でも、1つだけ聞きたいことがあった。

「ロックさん——1つ聞いてもいいですか」

「何かね」

「このPub/Subは、メッセージの配信を信頼性のある形で実現するんですよね。……通知の信頼性って、他にもパターンがあるんですか?」

ロックさんが座り直した。少し目が光った気がする。

テーブルの紙ナプキンを手に取り、万年筆で図を描き始めた。

1
2
3
4
5
6
7
Observer:     同じ部屋で声をかける(直接参照、同期、同一プロセス)
    ↓ 壁ができた
Domain Event: 何が起きたかを記録する(イベントオブジェクト、命名)
    ↓ 壁の向こうに伝えたい
Pub/Sub:      掲示板に貼り紙を出す(ブローカー仲介、非同期、プロセス間)
    ↓ 貼り紙が確実に作られたことを保証したい
Outbox:       貼り紙をDBと同じ引き出しに入れる(原子性の保証)

「Observer は声が届く範囲での通知だ。同じ部屋にいる限り、確実に届く。しかし部屋が壁で仕切られたら、声は届かない」

「Domain Event は何が起きたかを名前のあるオブジェクトとして記録する。“注文が確定した"という事実を、コールバックの引数ではなく、OrderConfirmed という独立した記録にする。——記録は残る。しかし、記録を壁の向こうに伝える仕組みは、Domain Event 単体では持っていない」

「Pub/Sub は壁の向こうに掲示板で伝える。Publisher は掲示板に貼る。Subscriber は掲示板を見に来る。壁を越えても通知が届く。——しかし、掲示板に貼り紙を出す瞬間にアプリケーションが落ちたら? ビジネスデータは更新されたが、貼り紙は出なかった?」

「それは——メッセージが消える問題……」

「Outbox パターンは、貼り紙をビジネスデータと同じトランザクションで保存する。貼り紙が確実に作られたことを、DBの原子性で保証する」

ナプキンの図を見つめた。4つの段階。Observer → Domain Event → Pub/Sub → Outbox。

「——通知の歴史とは、声が届く範囲を広げながら、信頼性を積み上げてきた歴史だ

その一言が、カフェの空気を変えた気がした。

「……わたしのシステムは、最初の段階にいたんですね。声が届く範囲でしか通知できないObserverを、壁の向こうに無理やり使っていた」

「Observerが悪いのではない。Observerは同じ部屋のために作られた。壁ができたなら、壁のための仕組みに切り替える。それだけのことだ」

ロックさんが立ち上がった。

「あの——報酬は」

「報酬?」

「佐藤さんが、キーボードで報酬を取るって聞いたので——」

少し笑った。この人が笑うのは珍しい気がする。初対面だからわからないけれど。

「キーボードは余っている。——次に壁にぶつかったとき、掲示板を思い出せ。それが報酬だ」

ドアベルが鳴って、ロックさんがカフェを出ていった。

しばらくその場に残った。テーブルに残ったナプキンの図と、自分のノートの見開き。

左ページにはカフェに来る前に描いたObserverの図。OrderService から3本の矢印が3つのサービスに直接伸びている。名前を知って、顔を見て、返事を待っていた。

右ページには今日描いたPub/Subの図。OrderService からTopicに1本の矢印。Topicから3つのサービスに3本の矢印。誰が読むかは掲示板が決める。

同じ通知なのに、矢印の意味が全部変わった。左のページでは、わたしが3人に声をかけていた。右のページでは、わたしは掲示板に1枚の紙を貼っただけだ。

壁の向こうに声は届かない。でも掲示板なら、見に来ればいい。

ノートを閉じた。PCを開いた。エディタに向かう。

1
package MessageBroker;

まず、掲示板から。


探偵の調査報告書

容疑(アンチパターン)真実(パターン)証拠(効果)
分散環境でのObserver直接呼び出し — SubjectがObserverのHTTPエンドポイントへの直接参照を保持し、同期的にnotifyを呼ぶ。1サービスの停止で全体が停止するPublish-Subscribe — メッセージブローカー(Topic)を仲介者とし、PublisherとSubscriberが互いを知らない疎結合な非同期通知Subscriberの障害がPublisherに波及しない。サービス追加にPublisherのコード変更が不要。障害が局所に閉じ込められる

推理のステップ

  1. Observerの直接参照を特定する: SubjectがObserverのリスト(エンドポイント)を保持し、同期的に呼び出していないか確認する
  2. メッセージブローカーを導入する: Topicを管理し、publishとsubscribeを仲介するMessageBrokerを作る
  3. Publisherロールを定義する: SubjectをPublisherに変える。ブローカーへの publish_event だけを持ち、Observerへの参照を捨てる
  4. Subscriberロールを定義する: handle メソッドを requires とするロールを作り、各サービスが実装する
  5. ブートストラップで接続する: アプリケーション起動時にSubscriberをトピックに登録する。PublisherとSubscriberはこの接続を知らない
  6. 冪等性を考慮する: at-least-once deliveryに対応するため、Subscriberのハンドラをメッセージ ID で冪等にする

ロックより

Observerは声が届く距離のためのパターンだ。壁ができたなら、壁のための仕組みに切り替えたまえ。掲示板に貼り紙を出す——それだけで、声は壁を越える。通知の歴史とは、声が届く範囲を広げながら信頼性を積み上げてきた歴史だ。君のシステムにも、その歴史を刻むがいい。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。