Featured image of post コードメカニック【Mediator】部品は全部直したのに、最後に全部が絡んでいた〜直結をやめ、仲介役にすべてを捌かせる〜

コードメカニック【Mediator】部品は全部直したのに、最後に全部が絡んでいた〜直結をやめ、仲介役にすべてを捌かせる〜

キャンセル機能を足したら、同じ確認メールが客に二通届いた。在庫・課金・通知・配送は綺麗に分けたのに、繋ぎ方だけが直結のN対Nで、通知を出す係が配線に散っていた。部品同士の直結をやめ、仲介役に連絡を集約するMediatorの整備記録。

返金が、止まった

「お客様から、同じメールが二通届くと——」。その報告がカスタマーサポートのチャンネルに流れたのは、注文キャンセル機能をリリースした、翌朝のことだった。

わたしは自社ECの注文システムを預かっている。チームのリードで、もう七年。人が物を買って、金を払って、箱が届く——その一連を回す、会社の背骨みたいなシステムだ。

最初、CSのチャンネルは静かにざわついていただけだった。「キャンセル確認メールが二通きた、と問い合わせが」。わたしは、表示側の不具合だろうと高をくくっていた。だが午前の半ばには、別の声が混じりはじめた。「在庫が、合いません」。キャンセルされた商品が、戻っていたり、戻っていなかったり。数が、ずれている。

確認メールが二通。在庫の数が、ずれる。どちらも、昨日足したキャンセル処理に触っている。わたしは返金のバッチを止めた。間違った数で返金を走らせるわけにはいかない。止めた瞬間、数百件のキャンセルが宙吊りになった。お客様の手元で、返金が、止まった。事業が、止まったということだ。

正直に言えば、わたしには自負があった。このシステムは、わたしがチームと一緒に、何年もかけて育ててきた。引き継いだ当初は、注文も在庫も課金も発送も、何もかもが一つの巨大なかたまりに詰まっていた。それを少しずつ、在庫・課金・通知・配送と、独立したサービスに切り分けてきた。一つひとつのサービスは、今では誇れるくらい綺麗だ。テストもある。役割も、はっきり分けた。

なのに、キャンセルを一つ足しただけで、なぜこうなる。

藁にもすがる気持ちで、取引先が前に世話になったという「出張整備の親方」に連絡を入れた。コードの整備を、出張でやる人がいる、と。半信半疑だった。昼過ぎ、その人はフロアに現れた。作業着の上に革のエプロン、片手に金属のツールボックス。世間話はしない。わたしが「見てもらえますか」と言うと、小さくうなずいた。

わたしは、つい先回りして弁護していた。「言っておくと、各サービスは、それぞれ綺麗に分けてあるんです。在庫も、課金も、通知も、配送も。役割はちゃんと——」。

親方は、わたしのモニタにも、燃えているCSのチャンネルにも、すぐには目をくれなかった。代わりに、わたしの方を見て、静かに口を開いた。

「——綺麗な部品が、四つ」。そして、続けた。「その四つは、互いに、何本の線で繋がってる」。

わたしは、答えに詰まった。

何本。……数えたことが、なかった。部品を磨くことはずっと考えてきた。在庫を綺麗にし、課金を綺麗にし。けれど、その部品と部品の間を結ぶ線が、いま何本あるのか——意識したことが、一度もなかった。

親方は答えを待たず、コードを開いた。まず、キャンセルで足したあたりだ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// agents/code-mechanic/tests/spaghetti-coupling-mediator/before/order.go
// 在庫・課金・通知・配送を、1つのパッケージに押し込んでいる。
// 別パッケージに分けると、互いの参照が循環インポート(import cycle)になり、コンパイルが通らないからだ。
package before

type Order struct {
	ID     string
	SKU    string // 商品管理番号
	Qty    int    // 注文数量
	Amount int    // 請求金額(円)
}

// InventoryService は在庫を担う。
// stock は SKU ごとの在庫数を持つ map[string]int(キーが SKU、値が在庫数の連想配列)。
type InventoryService struct {
	stock        map[string]int
	billing      *BillingService      // *BillingService は BillingService の実体を指す参照。これがサービス間の"配線"だ
	notification *NotificationService // 在庫 → 通知
	shipping     *ShippingService     // 在庫 → 配送
}

// (s *InventoryService) はメソッドレシーバ——「この関数は InventoryService に紐づく」という宣言。
func (s *InventoryService) Reserve(o Order) error {
	if s.stock[o.SKU] < o.Qty {
		s.notification.SendOutOfStock(o) // 在庫切れを、自分で通知
		return ErrOutOfStock             // 在庫切れを表すエラー値
	}
	s.stock[o.SKU] -= o.Qty
	return s.billing.Charge(o) // 確保できたら、課金を自分で呼ぶ
}

// Restore は在庫を元に戻す(キャンセル時に追加。Refund とは別の担当者が書いた)。
func (s *InventoryService) Restore(o Order) {
	s.stock[o.SKU] += o.Qty
	s.notification.SendCancellation(o) // ★ここでも客にキャンセル確認を送る(二通の片方)
}

在庫サービスは、課金・通知・配送のサービスを、自分のフィールドに直接抱えている。注文が来れば在庫を確保し、確保できたら、その手で課金サービスを呼ぶ。在庫切れなら、その手で通知を呼ぶ。

課金サービスも、同じ作りだった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// BillingService も、他のサービスを直接抱えている。
type BillingService struct {
	inventory    *InventoryService    // 課金 → 在庫(差し戻し用)
	notification *NotificationService // 課金 → 通知
	shipping     *ShippingService     // 課金 → 配送
	charged      int                  // 課金済み合計
	refunded     int                  // 返金済み合計(課金の charged と対)
}

func (s *BillingService) Charge(o Order) error {
	s.charged += o.Amount              // 課金処理(外部決済の呼び出しは省略し、合計だけ記録)
	s.notification.SendConfirmation(o) // 課金できたら、確認通知を自分で呼ぶ
	s.shipping.Arrange(o)              // 配送も、自分で呼ぶ
	return nil
}

// Refund は返金する(キャンセル時に追加)。
func (s *BillingService) Refund(o Order) {
	s.refunded += o.Amount
	s.inventory.Restore(o)             // 在庫を戻す(→ この Restore も SendCancellation を呼ぶ)
	s.notification.SendCancellation(o) // ★キャンセル確認を送る(二通のもう片方)
}

通知(NotificationService)と配送(ShippingService)の二つも、同じ作りだ。SendConfirmationArrange といったメソッドを持ち、やはり互いに他のサービスを直接抱えている。四つとも、そうやって手を繋ぎ合っている——だから、この四つを別々のパッケージに分けられない。在庫が課金を参照し、その課金もまた在庫を参照する。こういう相互参照があると、Go はその二つを別のパッケージに置けない。これを循環インポートという。仕方なく、四つを一つのパッケージに押し込んでいた。それ自体が、絡まりの証拠だ。

見てほしいのは、Refund と、Refund が呼んでいる Restore だ。返金サービスの Refund は、返金して、在庫の Restore を呼んで、そして客にキャンセル確認を送る。ところが、呼ばれた側の Restore も、在庫を戻したあとで、客にキャンセル確認を送っている。

「ここです」と、わたしは言った。声が小さくなった。「キャンセル確認を送っているのが……二箇所、ある」。

Refund を書いたのは、課金まわりの担当だ。Restore を書いたのは、在庫まわりの別のメンバーだった。どちらも善意で、「自分の処理の締めに、お客様へ確認を送っておくべきだ」と考えた。それぞれの判断は、間違っていない。ただ、二人とも、相手も同じことをしているとは、知らなかった。

「ちゃんと別々のサービスに分けたんです」と、わたしは言った。「なのに、なぜ、二重に……」。

とりあえず、一通に戻す

親方は、応急処置から入った。Restore の中の、キャンセル確認を送る一行を、コメントアウトした。

1
2
3
4
func (s *InventoryService) Restore(o Order) {
	s.stock[o.SKU] += o.Qty
	// s.notification.SendCancellation(o) // ← 応急: いったん止める。送るのは Refund 側だけにする
}

「これで、メールは一通になる」。親方の手つきは、ドライだった。

実際、ローカルで動かすと、キャンセル確認はきっちり一通だけ飛んだ。わたしは、少しほっとした。「直った……?」。

「これは応急だ」。親方は、手を止めて言った。「配線そのものが、絡んでる。これは、今日のメールを一通に戻しただけだ」。

わたしは、食い下がった。「でも、重複は消えました。Restore から送るのをやめれば、二度と二通にはならないですよね」。最後まで、言い切った。

親方は、RefundRestore の二箇所を、順番に指でさした。「キャンセル確認を送る係が、いま、二人いた。今日は、そのうちの一人を消した。だが——」。指が、配送サービスのあたりへ動く。「明日、配送にもキャンセル通知を足したくなったら、三人目の係ができる。その三人目が、また送る。消して回るのか、毎回」。

わたしは、黙った。

そうだ。重複が起きたのは、たまたま RefundRestore の二箇所が送っていたからじゃない。「誰がキャンセル確認を送るのか」という判断が、サービスのあちこちに、ばらばらに置かれているからだ。一箇所消しても、判断は散らばったまま。次に何かを足せば、また別のどこかで、同じことが起きる。

「とりあえず一通に戻ったのに、何がまずいんですか」。わたしは、自分で答えが見えはじめているのを感じながら、それでも訊いた。

「報せる係が、配線の中に散らばってる」。親方は短く言った。「散らばってる限り、足すたびに増える」。

六組、握り合ってる

「原因を、見せる」。親方はそう言って、画面を上にスクロールした。各サービスの、構造体の頭——他のサービスを抱えているフィールドの並びを、指で、一本ずつ手繰りはじめた。

「在庫と、課金。在庫と、通知。在庫と、配送」。指が、三つの組をなぞる。「課金と、通知。課金と、配送」。さらに二つ。「通知と、配送」。最後の一つ。

「六組」。親方は言った。「どの二つも、直接、手を握り合ってる。配線ってのは、二点を結ぶ一本だ。四つの部品で、六本」。

さっき答えられなかった問いの、答えだった。何本繋がっているか。六本。わたしのシステムは、四つの部品が、互いを直接つかんで、六本の線でぐるぐる巻きになっていた。

「これが原因だ」と親方は言った。これが、その状態の名前だった。

spaghetti coupling(スパゲッティ結合)——部品同士が互いを直接参照し合い、一つを変えると、繋がった全部に変更が波及してしまう状態。皿の上で絡まったスパゲッティのように、一本を引っぱると、関係ないはずの麺まで一緒に動く。

「キャンセルを一つ足すのに」と親方は続けた。「お前さんは、この六組の配線を、ぜんぶ気にして手を入れなきゃならなかった。在庫にもキャンセルの段取り、課金にも、通知にも。だから、送る係が二人になっても、気づけない。全部が見えないからだ」。

親方は、指を立てて数えた。「部品が一つ増えるたび、握り合う組は増える。四つで六組。五つなら十組。六つなら十五組。増えるのは、機能じゃない。配線のほうだ」。

わたしは、マウスから手を離した。画面の、その六組の線を、すぐには見られなかった。膝の上で、手を握った。

「……部品は、一個ずつ、綺麗にしたんです」。やっと、それだけ言った。「在庫も、課金も、それぞれは。でも」——言葉に詰まった。「繋ぎ方だけ、昔のままだった。直結で。部品を磨くことばっかり考えて、配線は……増えるに、任せてた」。

親方は無言のまま、私の肩越しにモニタを指差し、ツールボックスから取り出した赤ペンで、手元のメモ用紙に素早く線を走らせた。

「いいか。あんたがやったのは、こういうことだ」

親方の描いた図は、かつて私が誇らしく思った「独立した部品たち」のはずだった。しかし、そこに引かれた線はあまりにも不格好で、まるで車体の底でとぐろを巻く古い銅線の束のようだった。

親方の問いに答えられなかった理由が、ここで、自分でも腑に落ちた。数えたことがなかったのは、見ていなかったからだ。

思えば、このシステムは最初、何でも一つに詰まったかたまりだった。God Object——一つのものに役割が集まりすぎて、どこを触っても全体に響いてしまう、あの状態。それを部品に割ったのが、わたしがこのシステムでやった、最初の仕事だった。割ったのに。割った部品を、今度は六本の線で、また縛り直していた。

絡まりを、絵にするとこうだ。4つのサービス(INVENTORY/在庫、BILLING/課金、NOTIFICATION/通知、SHIPPING/配送)が、互いを直接つかみ合っている(線一本が、結合一組を表す)。

Directly coupled messy wiring diagram showing InventoryService, BillingService, NotificationService, and ShippingService steel plates connected directly to each other with tangled orange wires

「最初は、これで十分だと思ってたんです」と、わたしは言った。「サービスが、二つか三つの頃は」。

「二つなら、一本」と親方は言った。「三つなら、三本。そこまでは、直結でも手が回る。四つで、手が、足りなくなった」。

報せる先は、一つでいい

「直結を、やめる」。親方は、メモ用紙にもエディタにも図を描かず、ただ言葉で、組み直しの方針を置いた。「部品同士には、もう手を握らせない。代わりに、真ん中に一つ、捌く係を立てる。部品は、その係にだけ報せる。次に誰を動かすかは、係が決める」。

車でいう、中継ボックスだ。電装品を一つひとつ直結でつなぐのをやめて、配線をいったん一つの箱に集める。それと同じことを、コードでやる。

これが、その定石の名前だった。

Mediator(メディエーター・仲介者)——部品同士を直接やり取りさせず、仲介役を一つ立てて、すべての連絡をそこ経由にする整備の定石

親方は、まず、仲介役との約束事を書いた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// agents/code-mechanic/tests/spaghetti-coupling-mediator/after/event.go
package after

// Event は「システムの中で起きたこと」を表す文字列型。
// type Event string は、文字列に別名をつけた型。定数で並べておくと、打ち間違いをコンパイル時に防げる。
type Event string

const (
	EventStockReserved Event = "stock.reserved"  // 在庫を確保した
	EventOutOfStock    Event = "stock.out"        // 在庫が足りなかった
	EventCharged       Event = "billing.charged"  // 課金した
	EventOrderCanceled Event = "order.canceled"   // 注文がキャンセルされた
)

// Mediator は「次に誰を動かすか」を捌く、仲介役の約束事。
// interface は「Notify というメソッドさえ持っていれば、満たしたと見なす」決まり。Go は implements 宣言が要らない。
type Mediator interface {
	Notify(sender string, ev Event)
}

interface——ここで出てくる——は、「このメソッドさえ持っていれば、仲間として扱う」という約束事だ。ここでは Notify 一つ。誰が何を起こしたか(senderEvent)を、仲介役に知らせる口だけを、決めている。なお sender(誰が報せたか)は、このあとの仲介役では実は使っていない——次に誰を動かすかは「何が起きたか(ev)」だけで決まるからだ。あとで送り主ごとにログを残したくなったとき用に、口だけ開けてある。

次に、各サービスを、この約束に合わせて組み替えた。在庫サービスは、こう変わった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// agents/code-mechanic/tests/spaghetti-coupling-mediator/after/order.go
// 他サービスへの直接参照は、ゼロ。仲介役(mediator)だけを知る。
type InventoryService struct {
	stock    map[string]int
	mediator Mediator // 配線は、これ一本だけ
}

func (s *InventoryService) Reserve(o Order) error {
	if s.stock[o.SKU] < o.Qty {
		s.mediator.Notify("inventory", EventOutOfStock)
		return ErrOutOfStock
	}
	s.stock[o.SKU] -= o.Qty
	s.mediator.Notify("inventory", EventStockReserved) // 「確保した」と知らせるだけ
	return nil
}

func (s *InventoryService) Restore(o Order) {
	s.stock[o.SKU] += o.Qty // 戻すだけ。通知の手配は、仲介役に任せる
}

在庫を確保する中身——s.stock[o.SKU] -= o.Qty——は、前と一文字も変えていない。変わったのは、その後ろだ。前は、確保できたら自分で課金サービスを呼んでいた。今は、「確保した」と仲介役に報せて、終わり。次に課金を動かすかどうかは、自分では決めない。

課金サービスの Refund は、もっと分かりやすく変わった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (s *BillingService) Charge(o Order) error {
	s.charged += o.Amount
	s.mediator.Notify("billing", EventCharged) // 「課金した」と知らせるだけ
	return nil
}

func (s *BillingService) Refund(o Order) {
	s.refunded += o.Amount
	// 在庫の差し戻しも、キャンセル確認も、ここでは呼ばない。仲介役が捌く。
}

Refund が、返金するだけになった。在庫を戻す呼び出しも、キャンセル確認を送る呼び出しも、消えている。二重通知の片方が、ここから、いなくなった。

そして、捌く係——仲介役の本体だ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// agents/code-mechanic/tests/spaghetti-coupling-mediator/after/mediator.go
// OrderMediator は注文処理の仲介役。4サービスを持ち、switch で「次は誰か」だけを捌く。
// 持つのは、処理中の order だけ。在庫の減らし方や課金の中身は、各サービスに置いたまま。
type OrderMediator struct {
	inventory    *InventoryService
	billing      *BillingService
	notification *NotificationService
	shipping     *ShippingService
	order        Order
}

func (m *OrderMediator) Notify(sender string, ev Event) {
	switch ev {
	case EventStockReserved:
		m.billing.Charge(m.order) // 在庫を確保できた → 次は課金
	case EventCharged:
		m.notification.SendConfirmation(m.order) // 課金できた → 確認通知
		m.shipping.Arrange(m.order)              // → 配送手配
	case EventOutOfStock:
		m.notification.SendOutOfStock(m.order)
	case EventOrderCanceled:
		m.billing.Refund(m.order)
		m.inventory.Restore(m.order)
		m.notification.SendCancellation(m.order) // キャンセル確認は、ここ一箇所だけ
	}
}

最後に、これらをどう繋ぐかだ。親方は、組み立ての部分を見せた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func NewOrderSystem(initialStock map[string]int) ( /* 4サービスと仲介役を返す */ ) {
	med := &OrderMediator{}

	// 各サービスには、仲介役だけを渡す(他のサービスは渡さない)
	inv := &InventoryService{stock: initialStock, mediator: med}
	bil := &BillingService{mediator: med}
	// 通知・配送も、同じ要領で仲介役だけを渡す

	// 仲介役のほうが、4つのサービスを束ねて持つ
	med.inventory = inv
	med.billing = bil
	// med.notification, med.shipping も同様

	return inv, bil /* , ... */, med
}

配線をセットするのは、この組み立ての一箇所だけになった。サービス側には、もう「どのサービスを持つか」という配線が、一本も書かれていない。

そして、繋いだあとの流れは、こうだ。客が注文すると、まず在庫の Reserve が呼ばれる。在庫は確保できたら、自分で次を呼ぶ代わりに「確保した(EventStockReserved)」と仲介役に報せる。報せを受けた仲介役は、switch でその次——課金——を呼ぶ。課金できたら、今度は課金が「課金した(EventCharged)」と報せ、仲介役がまた次——確認通知と配送——を呼ぶ。バトンが、毎回いったん仲介役を経由して、次の部品へ渡っていく。部品は、隣の部品を知らない。知っているのは、渡し先の仲介役だけだ。

わたしは、EventOrderCanceled のところを、何度か読んだ。

「キャンセルの通知が……ここ、一行だけになってる」。声に出していた。「Refund からも、Restore からも、送るのが消えて。キャンセルのときに何をするかが、この三行に、全部ある」。

「報せる先が、一つになった」と親方は言った。「誰がキャンセル確認を出すか。ここを読めば、それで全部わかる。配線の中に、埋もれない。だから——二回には、ならない」。

二重通知が消えたのは、フラグで止めたからでも、片方を消したからでもなかった。「誰がそれを呼ぶか」という判断が、仲介役の一箇所に集まったから、構造として、二回になりようがなくなった。それが、応急処置との違いだった。

一つ、引っかかっていたことを訊いた。前に別の現場で、在庫の動きを複数の画面に知らせるのに、Observer を入れたことがあった。Observer——起きたことを一本の線で報せておいて、聞きたい側が勝手に拾う仕組みだ。それと、これは何が違うんだろう。

「どっちも、“知らせてる"ように見えるんですけど」と、わたしは訊いた。

「Observer は、知らせて終わりだ」と親方は言った。「誰が拾うかは、知らせる側は気にしない。一方通行の、放送だ。これは——拾った先で、次に誰を動かすか、仲介役が決める。放送じゃなく、交通整理だ。線の数は似てても、向きが違う」。

放送と、交通整理。Observer は、起きたことを撒くだけ。Mediator は、起きたことを受けて、次にどの部品を動かすかを、選んで指図する。だから仲介役の中には switch がある。誰の次は、誰か。それを、決めている。

そこまで腑に落ちて、わたしは、怖いことに気づいた。「でも……全部、この仲介役に集めたら」。言いながら、最初に割ったかたまりのことを、思い出していた。「これ、最初に割った、あのかたまり——God Object——に、逆戻りしませんか。仲介役が、また、何でも屋になる」。

親方は、否定しなかった。「戻る。書き方を、間違えれば」。

あっさり認めたので、わたしは、かえって身構えた。

「だから」と親方は続けた。「仲介役には、“順番"だけ持たせる。在庫を実際に減らすのも、金を取るのも、中身は、部品に置いたままだ。仲介役が書くのは、『確保できたら、次は課金』——誰の次は誰か、それだけ。ここに『在庫の減らし方』を書きはじめたら、その時はじめて、かたまりに戻る」。

わたしは、もう一度、仲介役のコードを見た。switch の中には、本当に「次は誰か」しか書いていない。在庫の数を触る行も、金額を計算する行も、一つもない。「捌くだけ」と、わたしは言った。「仕事の中身は、持たせない」。

親方は深くうなずき、メモ用紙の別の場所に、新しい図を力強く描き殴った。

「これが、中継ボックスを置いた後の姿だ」

ペン先が紙を叩く乾いた音が、静かなフロアに響く。今度の図は、さっきの絡まったスパゲッティとは全く違っていた。鋼鉄のプレートたちが、真ん中の一点を介して、まるで理路整然と並ぶ計器のように接続されている。

「配線が、整理された……」

私は呟いた。部品から伸びる線は、もう互いを締め付けるように絡み合ってはいない。

仲介役(ORDER MEDIATOR)を真ん中に据え、絡まりが解けると、こうなる。

Decoupled star topology schematic diagram showing OrderMediator steel plate in the center with bidirectional orange wires to InventoryService and BillingService, and unidirectional orange wires to NotificationService and ShippingService

六組あった線が、四本になった。どの部品も、仲介役とだけ、繋がっている。

矢印が、すべて「サービス→仲介役」の一方向になった。さっきまでは、在庫と課金が互いを指し合って、輪っかになっていた——だから、別々のパッケージに分けられなかった。その輪が、切れた。これで、各サービスを別のパッケージに切り出しても、もう循環インポートにはならない。仲介役という一点を介して、依存が一方通行になったからだ。

在庫がずれていたのも、根は同じだった。キャンセルのとき、在庫を戻す段取りも、確認を送る段取りも、あちこちのサービスに散らばっていた。だから、戻りすぎたり、戻し漏れたりした。それが今は、仲介役の EventOrderCanceled の一箇所に、順番に並んでいる。後始末が、もう散らばらない。

鍵は、あんたのものだ

試運転は、テストで通した。

確定の流れ——在庫を確保し、課金し、確認を送り、配送を手配する——の結果は、整備の前と後で、ぴったり同じだった。最終的な在庫の数も、課金した額も、配送の伝票も、一致する。挙動は、変えていない。変えたのは、繋ぎ方だけだ。

そして、今朝の事故。キャンセルを一回かけて、確認メールが何通飛ぶか。

1
2
3
$ go test ./...
ok  	code-mechanic/spaghetti-coupling-mediator/before	0.382s
ok  	code-mechanic/spaghetti-coupling-mediator/after	0.399s

整備前のコードには、「キャンセルで確認が二通飛ぶ」ことを、そのまま書き留めたテストを残した。今朝の事故の、記録だ。整備後のテストは、同じ条件で、確認が一通だけになることを確かめている。同じ操作、違う結果。緑が、並んだ。

「新しいサービスを足すときは」と親方は言った。「もう、部品同士は触らない。仲介役の switch に、一行、足すだけだ」。ポイントの付与でも、領収書の発行でも、配線するのは、その一箇所。六組を気にして回ることは、もう、ない。

わたしは、緑のテストを見ながら、自分のシステムの来歴を、口にしていた。「在庫を、別チームのAPIでも差し替えられるようにしたのも——外から挿せるようにした、あれ。注文の状態を、注文自身に持たせたのも。あれも、これも……全部、結合を、緩める話だったんだ。一個ずつ、緩めてきた。最後に残ってたのが、緩めた部品同士の、繋ぎ方だった」。

親方は、ツールボックスの蓋を閉じた。金属の、低い音がした。「あの、お礼は」と、わたしは切り出した。

親方は、いつもそうしているのか、報酬の話には乗らなかった。代わりに、短く言った。「もう、私を呼ぶな。——それが、一番の褒め言葉だ」。

それから、わたしのデスクの端に置いてあった、サーバルームの鍵を、わたしの方へ滑らせた。預かっていた車を、客に返すみたいに。「鍵は、あんたのものだ」。

去り際、親方は——これまで、こういう人は振り返らずに出ていくものだと思っていたが——一度だけ足を止めて、わたしの方を、まっすぐに見た。

「いい部品に、育てたな」。そして、付け加えた。「あとは、繋ぎ方だけ、だった」。

それだけ言って、出ていった。わたしは、その背中に、浅く頭を下げた。

仲介役のコードを、もう一度、上から読む。部品から伸びる線が、全部、真ん中の一点に集まっている。どこにも、絡まっていない。次にこのシステムが何かと絡んだとしても——今度は、自分で、解ける気がした。


整備記録簿

こんな異音・症状が出たら入れるべき整備(パターン)まだ様子見でいい
機能を一つ足すたび、関係ないサービスまで巻き込んで複数箇所を直す。サービス同士が互いを直接参照している 仲介役(Mediator)を立て、各サービスは仲介役にだけ通知する
同じ通知が複数回飛ぶ・抜ける。「誰がそれを呼ぶか」がサービスのあちこちに散っている 「次に誰を動かすか」を仲介役の一箇所(switch)に集約する
サービスを別パッケージ・別モジュールに切り出そうとすると循環インポートになる 依存を「各サービス → 仲介役」の一方向にして、循環を断つ
サービスが二〜三個で、相互参照も浅い 直結のままで十分。仲介役は過剰

整備手順

  1. 各サービスが、他のどのサービスを直接参照しているかを数える。「二点を結ぶ一本」の組がいくつあるか(N個の部品なら最大で N×(N−1)÷2 組)。組が増えて、一つの変更が全体に波及しはじめていたら、spaghetti coupling を疑う。
  2. 仲介役の約束事を定義する。type Mediator interface { Notify(sender string, ev Event) } のように、「起きたことを知らせる口」を一つ決める。イベントは type Event string と定数で並べ、取り違えを防ぐ。
  3. 各サービスから、他サービスへの直接参照(フィールド)を消し、mediator 一本にする。処理の最後で「自分で次を呼ぶ」のをやめ、mediator.Notify(...) で「起きたこと」を知らせるだけにする。業務の中身(在庫を減らす・課金する)は、サービスに残す。
  4. 仲介役の本体に、「誰の次は誰か」を switch で書く。在庫を確保できたら課金、課金できたら通知と配送、というように、順番だけを一箇所に集める。同じ処理を呼ぶ判断(通知など)が散らばっていたら、ここに一本化する。
  5. 整備の前後で、正常系の結果(在庫数・課金額・配送)が変わらないことをテストで確かめる。今回直したかったバグ(重複・抜け)は、前のコードに「事故の記録」として書き留め、後のコードで解消されることを回帰テストで示す。

親方より

部品を一つずつ綺麗にするのは、正しい。だが、部品が綺麗でも、繋ぎ方を直結のまま増やせば、いつか手が足りなくなる。四つで六組、五つで十組。増えるのは、配線のほうだ。一つ変えるたびに全部に響くようになったら、それが頃合いだ。部品同士に手を握らせるのをやめて、真ん中に、捌く係を一つ立てろ。

ただし、やりすぎ注意。仲介役に業務ロジックまで書きはじめたら、それは新しい God Object だ。仲介役は、「次に誰か」の調停だけに、保て。中身は、部品に置いておけ。

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