「助けてください! 通知システムのサブクラスが30個以上あって、もう1個も増やせないんです!」
私は佐伯。中堅SaaS企業でバックエンドを担当して5年になる。先月まではそれなりに自信もあった。通知システムの保守を引き継ぐまでは。
前任の山岸先輩は、退職前に分厚い引き継ぎ資料を残してくれた。そこには「通知機能を拡張するときは、既存クラスを継承して新しいサブクラスを作ること」と書かれていた。親切な指示だと思った。最初の1週間だけは。
メール通知。Slack通知。リトライ付きメール通知。ログ付きSlack通知。リトライ+ログ付きメール通知。リトライ+ログ付きSlack通知――。
サブクラスの一覧をExcelに書き出したとき、私は自分の目を疑った。30を超えていた。そして今朝、上司がにこやかに言った。
「佐伯さん、SMS通知も追加してもらえる? 来週までに」
その瞬間、私の視界が暗くなった。SMS通知を追加するということは、既存の組み合わせの数だけ新しいサブクラスが必要になるということだ。私はノートPCを抱え、雑居ビルの薄暗い階段を駆け上がった。
「レガシー・コード・インベスティゲーション(LCI)」
ガラス扉の文字が目に入る。ドアを引くと、サーバーラックの排熱とエナジードリンクの甘い残り香が鼻を突いた。革張りの椅子にふんぞり返った男が、私のノートPCを一瞥してから顔を見た。
「おやおや。ずいぶん分厚い紙束だね、ワトソン君。容疑者リストかね?」
「佐伯です。容疑者じゃなくてサブクラスの一覧です。30個以上あるんです」
自称「コード探偵」のロックは紙束を受け取り、鼻を近づけてにおいを嗅ぐような仕草をした。何をしているのかは聞かないことにした。
「ふむ……この犯人は変装の名人だよ、ワトソン君。30の顔を持っているが、正体は一つだ」
現場検証:三十の仮面
「まず全貌を見せたまえ」
私はクラスの継承ツリーを開いた。
classDiagram
BaseNotifier <|-- EmailNotifier
BaseNotifier <|-- SlackNotifier
EmailNotifier <|-- EmailRetryNotifier
EmailNotifier <|-- EmailLogNotifier
EmailNotifier <|-- EmailRetryLogNotifier
SlackNotifier <|-- SlackRetryNotifier
SlackNotifier <|-- SlackLogNotifier
SlackNotifier <|-- SlackRetryLogNotifier
class BaseNotifier {
+recipient
+send(message)
}
class EmailNotifier {
+send(message)
}
class SlackNotifier {
+send(message)
}
class EmailRetryNotifier {
+max_retries
+send(message)
}
class EmailLogNotifier {
+log
+send(message)
}
class EmailRetryLogNotifier {
+max_retries
+log
+send(message)
}
class SlackRetryNotifier {
+max_retries
+send(message)
}
class SlackLogNotifier {
+log
+send(message)
}
class SlackRetryLogNotifier {
+max_retries
+log
+send(message)
}
「見てください。EmailRetryNotifier、EmailLogNotifier、EmailRetryLogNotifier……メール側だけでこれです。Slack側にも全く同じ組み合わせがあって、リトライのロジックなんてコピペです」
ロックは椅子の背もたれに深く沈み込み、天井を見上げた。
「初歩的なにおいだよ、ワトソン君。犯人は組み合わせ爆発——継承の仮面を被り、分身を無限に増やす怪人だ」
【Before】サブクラスが爆発した通知システム
| |
ここまではいい。問題はここからだ。
| |
「そしてSlack側にも、まったく同じリトライとログのロジックがコピペされている」
| |
「通知先2種 × 機能3パターンで、すでにサブクラスが6つ。ここにフィルタ機能を足すと2の4乗で……ここにSMS通知を追加したら……」
私は計算するのをやめた。
ロックは立ち上がり、ホワイトボードに数式を書いた。
「通知先N種 × 機能の組み合わせ2のM乗。通知先3種で機能が4つなら、最大で48クラス。君の先輩は怪人四十八面相を量産していたのだよ」
「先輩を悪く言わないでください……でも、どうすればいいんですか? 継承以外に機能を追加する方法なんて……」
ロックは薄く笑った。
「変装を剥がすのではない。重ね着の技法を教えてやるのさ」
推理披露:重ね着の技法(Decorator)
「ワトソン君。君が着ているコートの上にマフラーを巻き、さらに帽子を被ったとしよう。コートもマフラーも帽子も、それぞれ独立した『装飾品』だ。コートを脱いでもマフラーは残る。帽子だけ外すこともできる」
「はあ……」
「だが今の君のコードは、『コート+マフラー』専用の服を1着、『コート+帽子』専用の服を1着、すべての組み合わせ分の一体型スーツを仕立てているのだよ。着替えるたびに全身を脱がなければならない」
言われてみれば、確かにそうだ。リトライとログを別々の「装飾品」として扱えれば、組み合わせごとに新しいクラスを作る必要はない。
「まず、共通のインターフェースを決める」
【After】共通ロール(インターフェース)の定義
| |
「Mooの Role だね。send メソッドを持つものはすべて Notifier として扱える。メール送信もSlack送信も、リトライもログも、全員がこの契約を守る」
【After】基本の通知クラス(具象コンポーネント)
| |
「SMS通知を追加するには SmsSender を1つ作るだけ。それだけだよ」
「え、本当にそれだけ……?」
「慌てるなワトソン君。ここからが本題だ」
ロックはホワイトボードに大きく「Decorator」と書いた。
【After】Decorator基底クラスと具象Decorator
| |
私は画面を食い入るように見つめた。
「NotifierDecorator が内側に notifier を持っていて、send を呼ぶと内側に委譲する……。各Decoratorは、委譲の前後に自分の仕事だけ追加しているんですね」
「その通り。証拠品を保護袋で包むようなものだ。中身はそのまま、外側の袋が機能を追加する。袋は何枚でも重ねられるし、どの順番でも構わない」
classDiagram
class Notifier {
<<Role>>
+recipient
+send(message)*
}
Notifier <|.. EmailSender
Notifier <|.. SlackSender
Notifier <|.. SmsSender
Notifier <|.. NotifierDecorator
NotifierDecorator <|-- RetryDecorator
NotifierDecorator <|-- LogDecorator
NotifierDecorator <|-- FilterDecorator
NotifierDecorator o-- Notifier : notifier
class EmailSender {
+send(message)
}
class SlackSender {
+send(message)
}
class SmsSender {
+send(message)
}
class NotifierDecorator {
+notifier
+send(message)
}
class RetryDecorator {
+max_retries
+send(message)
}
class LogDecorator {
+log
+send(message)
}
class FilterDecorator {
+filter_word
+send(message)
}
「そしてこれが、使う側のコードだ」
【After】Decoratorを自由に組み合わせる
| |
「30個のサブクラスが……基本3つとDecorator3つの、合計6つに?」
「すべての不吉な継承を排除して残ったものが、いかにシンプルであっても、それが真実(デザインパターン)なんだ」
解決:仮面の怪人の正体
ロックがテストを実行すると、ターミナルに整然とした結果が並んだ。
| |
「Beforeではメールとリトライの組み合わせのためだけに専用クラスが必要だった。Afterでは RetryDecorator がメールにもSlackにもSMSにも使える。一枚のマフラーがどのコートにも合うのと同じことだ」
「すごい……。じゃあ、もしWebhook通知を追加しろって言われたら?」
「WebhookSender を1つ作るだけだ。既存のDecoratorはそのまま使える」
「1つ……だけ?」
私は思わず目頭が熱くなった。あの30個のサブクラスを前に、毎晩遅くまでコピペを繰り返していた自分が馬鹿みたいだ。
「報酬は……そうだな、このクラス階層の深さと同じミリリットル数のバーボンをいただこうか。いや待て、Beforeの継承ツリーの深さだぞ。Afterのほうを基準にされては、舌を湿らすことすらできん」
「先輩のコードをここまで見事に片付けてくれたんです。バーボンくらいお安い御用です!」
「ふむ。ワトソン君、最後に一つ」
ロックは人差し指を立てた。
「Decoratorは実行時に自由に組み合わせたい場面で威力を発揮する。だが、組み合わせが常に固定で変わらないのなら、わざわざDecoratorにする必要はない。怪人二十面相を捕まえたからといって、すべての市民に変装術を教える必要はないのだよ」
私はPCを閉じて立ち上がった。来週のSMS通知、もう怖くない。
探偵の調査報告書
| 容疑(アンチパターン) | 真実(パターン) | 証拠(効果) |
|---|---|---|
| 組み合わせ爆発(Combinatorial Explosion)。機能の組み合わせごとにサブクラスを作成し、通知先N × 機能2のM乗のクラスが乱立。同じロジックが複数のサブクラスにコピペされ、新しい通知先や機能の追加で爆発的にクラス数が増加する。 | Decorator パターン。共通インターフェース(Role)を通じて、基本の通知クラスに機能を「重ね着」のように動的に追加する設計方式。各機能を独立したDecoratorクラスに分離し、委譲によって自由に組み合わせる。 | クラス数が30以上から約6に削減。新しい通知先の追加は1クラスのみ。リトライ・ログ・フィルタの各ロジックが1箇所に集約され、コピペが完全に解消。 |
推理のステップ
- 共通インターフェースを定義する: すべての通知クラスとDecoratorが守るべき契約(
sendメソッド)をRoleとして定義する。 - 基本コンポーネントを分離する: メール送信、Slack送信など、純粋な通知機能だけを持つクラスを作る。「送る」以外の責務を持たせない。
- Decorator基底クラスを作る: 内側に
notifierを保持し、デフォルトでは委譲するだけの基底クラスを用意する。 - 各機能をDecoratorとして実装する: リトライ、ログ、フィルタなど、付加的な機能をそれぞれ独立したDecoratorクラスに切り出す。委譲の前後に自分の処理を挟む。
- 利用側で自由に組み合わせる: 必要なDecoratorを必要なだけネストして、実行時に通知パイプラインを構築する。
ロックより
ワトソン君。継承は便利な道具だが、「機能の追加=サブクラスの追加」という思い込みに縛られると、クラスは恐ろしい速度で増殖する。怪人二十面相の変装のように、見かけは違っても中身は同じコピペが何十体も並ぶことになる。
Decoratorは「重ね着」の発想だ。コートの上にマフラーを巻き、その上に帽子を被る。どれも独立した装飾品であり、自由に着脱できる。コート専用マフラーや、マフラー一体型帽子を仕立てる必要はないのだよ。
ただし、重ね着の順番が結果に影響する場合がある。フィルタの後にリトライするのか、リトライの後にフィルタするのかで挙動は変わる。自由には責任が伴う。それを忘れなければ、君の通知システムは何十通りの組み合わせにも、たった数枚の衣装で対応できるだろう。
