「レガシー・コード・インベスティゲーション(LCI)」。 雑居ビルの2階にある、探偵事務所のような看板。私は藁にもすがる思いでそのドアを叩いた。
「助けてください。このままだと、ディレクトリがクラスファイルで埋め尽くされます……!」
私の名前はハルカ。入社1年目のバックエンドエンジニアだ。 現在、ECサイトの「ポイント付与システム」を任されているのだが、先輩からの指示通りに「正しいオブジェクト指向」で拡張を続けていたら、とんでもないことになってしまったのだ。
「ほう」
部屋の奥から、ヨレヨレのトレンチコートを羽織った男が現れた。エナジードリンクの空き缶が散乱するデスクの主、自称コード探偵のロックだ。
「ディレクトリが埋め尽くされる、ね。なかなかホラーな依頼じゃないか。さあワトソン君、現場を見せたまえ」
ワトソン君。その古臭い呼び名に訂正を入れる余裕すら、今の私にはなかった。 私はノートPCを開き、震える手でファイル一覧を表示した。
現場検証:増殖する血族たち
「最初はシンプルだったんです。ただポイントを計算するだけの PointService というクラスがありました」
「ふむ」
「でも、ビジネス側から『ポイント付与時にログを出してほしい』『特定の条件でメール通知してほしい』『期間限定でポイント2倍キャンペーンをやりたい』と、次々に要件が降ってきて……」
ロックがモニターを覗き込んだ。そこには、目を疑うようなファイル群が並んでいた。
| |
「先輩に『オブジェクト指向なんだから、既存のコードはいじらずに継承(サブクラス化)で拡張しろ』って教わったんです。だから、組み合わせが発生するたびに新しいサブクラスを作っていったら……」
「……機能が3つで、クラスが8個に増殖したというわけだね」
ロックはエナジードリンクを一口飲み、小さく笑った。
「そして来週、4つ目の機能『初回限定ボーナス』が追加される。君はさらに8個のクラスを追加して、合計16個のクラスを管理しなければならなくなる。そうだね?」
私は無言で頷いた。その通りだ。機能が1つ増えるたびに、クラスの数が倍々ゲームで増えていく。
「これがいわゆる『クラスの爆発(Combinatorial Explosion)』のにおいだ」
ロックは画面上の PointService::WithLogAndNotification.pm という、呪文のように長いクラス名を指差した。
| |
「ワトソン君。この容疑者は、防弾チョッキの上にダウンジャケットを着て、さらにレインコートとタキシードを重ね着している状態だ。すべてを『血縁(継承)』で解決しようとするから、無限の組み合わせを事前に用意しなければならなくなる」
「正しく継承を使っているはずなのに……どうすればいいんですか?」
「継承という重い鎖を断ち切り、**委譲(ラッピング)**という『重ね着』の魔法を見せてあげよう」
推理披露:重ね着の法則
ロックの指がキーボードの上で踊り始めた。
「まず、すべてのクラスが守るべき『約束』を定義する。ポイントを付与する、という振る舞いだけだ」
| |
「次に、純粋にポイントを計算するだけの『素体』を用意する。こいつはログもメールも知らない。ただの Tシャツ だ」
| |
「ここからが本番だ。ログやメール通知といった『追加機能』を、サブクラスではなく Decorator(装飾者) として切り出す」
ロックは新しいクラスを作り始めた。
| |
「inner……? 親を継承(extends)するんじゃなくて、別のオブジェクトを『持っている』んですか?」
「その通り。これが has-a(委譲) だ。そして、この型紙を使って『着せ替えパーツ』を作るんだ」
| |
「……クラスの数が減りました。でも、どうやって『ログを出して、かつメールも送る』を実現するんですか? WithLogAndNotification クラスがありませんよ?」
ロックは不敵に笑った。
「クラスを事前に用意する必要はない。実行するときに、好きなだけ重ね着させればいいんだよ」
動的な着せ替えマジック
ロックはテスト用のスクリプトを開き、コードを打ち込んだ。
| |
私は画面に釘付けになった。 マトリョーシカのように、オブジェクトが別のオブジェクトを包み込んでいる。
ターミナルの実行結果はこうだ。
| |
「すごい……! 全部の機能が、順番通りに動いてる……!」
「素体(Base)に、ポイント2倍(DoublePoint)を着せ、メール通知(Notification)を着せ、最後にログ(Log)を着せた。見事に全部入りの完成だ」
「じゃあ、来週追加される『初回ボーナス』も……」
「ボーナス機能だけを持ったクラスを1つ(WithFirstTimeBonus)作るだけでいい。組み合わせ用のクラスは一切不要だ。君のディレクトリは、もうこれ以上汚染されない」
事件の終わり:脱ぎ捨てられたコード
私は WithLogAndNotificationAndDoublePoint.pm をはじめとする、不格好な名前のクラスたちを次々と git rm していった。
心がすっと軽くなるのを感じた。
「先輩に言われた通りに『正しくオブジェクト指向』をやっているつもりだったのに、まさかこんな落とし穴があるなんて……」
「『継承』は強力だが、静的で柔軟性がない。機能を組み合わせるなら『委譲(has-a)』の方がずっと身軽なのさ。着膨れしたコードは、適切に脱がせてやるに限る」
ロックは満足げにコートの襟を立てた。(室内なのに)
「さて、見事に身軽になったところで報酬の話だが……今回は、スパイスの層が幾重にも重なった特製カレーの出前なんてどうだろう。もちろん、トッピング(Decorator)は全部のせで頼むよ」
私は苦笑しながらスマホを取り出した。 「わかりました。チーズにカツにほうれん草ですね。……胃袋まで爆発させないでくださいよ、探偵さん」
探偵の調査報告書
| 容疑(アンチパターン) | 真実(パターン) | 証拠(効果) |
|---|---|---|
| クラスの爆発(Combinatorial Explosion)。複数の機能の組み合わせをすべて「継承」で表現しようとし、サブクラスが無限に増殖する。 | Decorator パターン。既存のオブジェクトを、同じインターフェースを持つ「装飾オブジェクト」で包み込み(ラッピング)、機能を外側から追加する。 | 動的な機能の組み合わせ。機能がいくつ増えても、追加するクラスの数は「機能の数」だけで済む(掛け算から足し算への変換)。 |
推理のステップ
- 共通Roleの定義: 本体(Component)と装飾者(Decorator)が同じように振る舞えるよう、共通のインターフェースを定義する。
- Decorator基底クラスの作成: 内側に「共通Roleを実装したオブジェクト(inner)」を保持するクラスを作る。
- 具体的な装飾の実装: 各機能(ログ、メールなど)ごとにDecoratorのサブクラスを作り、処理の前後に独自のロジックを差し込む。
- 実行時の組み立て: クライアント側で、必要な分だけオブジェクトをマトリョーシカのように入れ子にして生成する。
ロックより
ワトソン君。オブジェクト指向を学んだばかりのエンジニアは、なんでもかんでも「継承」で解決しようとして自滅する傾向にある。
だが覚えておきたまえ。「親から受け継ぐ」ことと「道具として持たせる」ことは全く違う。機能の組み合わせに悩んだら、血縁(is-a)を疑い、重ね着(has-a)を試すんだ。君のコードは、もっとオシャレになれるはずだよ。
