Featured image of post コード探偵ロックの事件簿【Decorator】着膨れした容疑者〜クラス増殖トリックと重ね着の法則〜

コード探偵ロックの事件簿【Decorator】着膨れした容疑者〜クラス増殖トリックと重ね着の法則〜

継承の乱用が生み出した「クラスの爆発」。キャンペーンの組み合わせごとに増殖するコードを、探偵ロックがDecoratorパターンで鮮やかに着せ替える!

「レガシー・コード・インベスティゲーション(LCI)」。 雑居ビルの2階にある、探偵事務所のような看板。私は藁にもすがる思いでそのドアを叩いた。

「助けてください。このままだと、ディレクトリがクラスファイルで埋め尽くされます……!」

私の名前はハルカ。入社1年目のバックエンドエンジニアだ。 現在、ECサイトの「ポイント付与システム」を任されているのだが、先輩からの指示通りに「正しいオブジェクト指向」で拡張を続けていたら、とんでもないことになってしまったのだ。

「ほう」

部屋の奥から、ヨレヨレのトレンチコートを羽織った男が現れた。エナジードリンクの空き缶が散乱するデスクの主、自称コード探偵のロックだ。

「ディレクトリが埋め尽くされる、ね。なかなかホラーな依頼じゃないか。さあワトソン君、現場を見せたまえ」

ワトソン君。その古臭い呼び名に訂正を入れる余裕すら、今の私にはなかった。 私はノートPCを開き、震える手でファイル一覧を表示した。


現場検証:増殖する血族たち

「最初はシンプルだったんです。ただポイントを計算するだけの PointService というクラスがありました」

「ふむ」

「でも、ビジネス側から『ポイント付与時にログを出してほしい』『特定の条件でメール通知してほしい』『期間限定でポイント2倍キャンペーンをやりたい』と、次々に要件が降ってきて……」

ロックがモニターを覗き込んだ。そこには、目を疑うようなファイル群が並んでいた。

1
2
3
4
5
6
7
8
PointService.pm
PointService::WithLog.pm
PointService::WithNotification.pm
PointService::WithDoublePoint.pm
PointService::WithLogAndNotification.pm
PointService::WithLogAndDoublePoint.pm
PointService::WithNotificationAndDoublePoint.pm
PointService::WithLogAndNotificationAndDoublePoint.pm

「先輩に『オブジェクト指向なんだから、既存のコードはいじらずに継承(サブクラス化)で拡張しろ』って教わったんです。だから、組み合わせが発生するたびに新しいサブクラスを作っていったら……」

「……機能が3つで、クラスが8個に増殖したというわけだね」

ロックはエナジードリンクを一口飲み、小さく笑った。

「そして来週、4つ目の機能『初回限定ボーナス』が追加される。君はさらに8個のクラスを追加して、合計16個のクラスを管理しなければならなくなる。そうだね?」

私は無言で頷いた。その通りだ。機能が1つ増えるたびに、クラスの数が倍々ゲームで増えていく。

「これがいわゆる『クラスの爆発(Combinatorial Explosion)』のにおいだ」

ロックは画面上の PointService::WithLogAndNotification.pm という、呪文のように長いクラス名を指差した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package PointService::WithLogAndNotification {
    use Moo;
    extends 'PointService';

    sub add_points ($self, $user, $amount) {
        # ログ出力
        say "[LOG] Adding $amount points to " . $user->name;

        # 親クラス(本体)の処理を呼ぶ
        $self->SUPER::add_points($user, $amount);

        # メール通知
        say "[MAIL] Sent point notification to " . $user->email;
    }
}

「ワトソン君。この容疑者は、防弾チョッキの上にダウンジャケットを着て、さらにレインコートとタキシードを重ね着している状態だ。すべてを『血縁(継承)』で解決しようとするから、無限の組み合わせを事前に用意しなければならなくなる」

「正しく継承を使っているはずなのに……どうすればいいんですか?」

「継承という重い鎖を断ち切り、**委譲(ラッピング)**という『重ね着』の魔法を見せてあげよう」


推理披露:重ね着の法則

ロックの指がキーボードの上で踊り始めた。

「まず、すべてのクラスが守るべき『約束』を定義する。ポイントを付与する、という振る舞いだけだ」

1
2
3
4
5
6
7
# -----------------------------------
# 1. 共通のRole(インターフェース)
# -----------------------------------
package PointService::Role {
    use Moo::Role;
    requires 'add_points';
}

「次に、純粋にポイントを計算するだけの『素体』を用意する。こいつはログもメールも知らない。ただの Tシャツ だ」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# -----------------------------------
# 2. 素体(Component)
# -----------------------------------
package PointService::Base {
    use Moo;
    with 'PointService::Role';

    sub add_points ($self, $user, $amount) {
        say "[SYSTEM] $amount points added to " . $user->name;
        # 実際のDB更新処理など...
    }
}

「ここからが本番だ。ログやメール通知といった『追加機能』を、サブクラスではなく Decorator(装飾者) として切り出す」

ロックは新しいクラスを作り始めた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# -----------------------------------
# 3. 装飾の型紙(Decorator Base)
# -----------------------------------
package PointService::Decorator {
    use Moo;
    with 'PointService::Role';

    # 内側に「別のPointService」を抱え込む(has-aの関係)
    has inner => (
        is       => 'ro',
        does     => 'PointService::Role',
        required => 1,
    );

    sub add_points ($self, $user, $amount) {
        # デフォルトでは内側の処理をそのまま呼ぶだけ
        return $self->inner->add_points($user, $amount);
    }
}

inner……? 親を継承(extends)するんじゃなくて、別のオブジェクトを『持っている』んですか?」

「その通り。これが has-a(委譲) だ。そして、この型紙を使って『着せ替えパーツ』を作るんだ」

 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
37
38
# -----------------------------------
# 4. 具体的な着せ替えパーツ(Concrete Decorators)
# -----------------------------------

# ログを出力するジャケット
package PointService::WithLog {
    use Moo;
    extends 'PointService::Decorator';

    sub add_points ($self, $user, $amount) {
        say "[LOG] Start adding points...";
        $self->inner->add_points($user, $amount);
        say "[LOG] End adding points.";
    }
}

# メールを送信するレインコート
package PointService::WithNotification {
    use Moo;
    extends 'PointService::Decorator';

    sub add_points ($self, $user, $amount) {
        $self->inner->add_points($user, $amount);
        say "[MAIL] Sent notification to " . $user->email;
    }
}

# ポイントを2倍にする魔法のマント
package PointService::WithDoublePoint {
    use Moo;
    extends 'PointService::Decorator';

    sub add_points ($self, $user, $amount) {
        my $doubled = $amount * 2;
        say "[CAMPAIGN] Points doubled: $amount -> $doubled";
        $self->inner->add_points($user, $doubled);
    }
}

「……クラスの数が減りました。でも、どうやって『ログを出して、かつメールも送る』を実現するんですか? WithLogAndNotification クラスがありませんよ?」

ロックは不敵に笑った。

「クラスを事前に用意する必要はない。実行するときに、好きなだけ重ね着させればいいんだよ

動的な着せ替えマジック

ロックはテスト用のスクリプトを開き、コードを打ち込んだ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# ユーザーの用意
my $user = User->new(name => 'Haruka', email => 'haruka@example.com');

# 1. 全部盛りのサービスを「その場で作る」
my $service = PointService::WithLog->new(
    inner => PointService::WithNotification->new(
        inner => PointService::WithDoublePoint->new(
            inner => PointService::Base->new()
        )
    )
);

# 実行
$service->add_points($user, 100);

私は画面に釘付けになった。 マトリョーシカのように、オブジェクトが別のオブジェクトを包み込んでいる。

ターミナルの実行結果はこうだ。

1
2
3
4
5
[LOG] Start adding points...
[CAMPAIGN] Points doubled: 100 -> 200
[SYSTEM] 200 points added to Haruka
[MAIL] Sent notification to haruka@example.com
[LOG] End adding points.

「すごい……! 全部の機能が、順番通りに動いてる……!」

「素体(Base)に、ポイント2倍(DoublePoint)を着せ、メール通知(Notification)を着せ、最後にログ(Log)を着せた。見事に全部入りの完成だ」

「じゃあ、来週追加される『初回ボーナス』も……」

「ボーナス機能だけを持ったクラスを1つ(WithFirstTimeBonus)作るだけでいい。組み合わせ用のクラスは一切不要だ。君のディレクトリは、もうこれ以上汚染されない」


事件の終わり:脱ぎ捨てられたコード

私は WithLogAndNotificationAndDoublePoint.pm をはじめとする、不格好な名前のクラスたちを次々と git rm していった。 心がすっと軽くなるのを感じた。

「先輩に言われた通りに『正しくオブジェクト指向』をやっているつもりだったのに、まさかこんな落とし穴があるなんて……」

「『継承』は強力だが、静的で柔軟性がない。機能を組み合わせるなら『委譲(has-a)』の方がずっと身軽なのさ。着膨れしたコードは、適切に脱がせてやるに限る」

ロックは満足げにコートの襟を立てた。(室内なのに)

「さて、見事に身軽になったところで報酬の話だが……今回は、スパイスの層が幾重にも重なった特製カレーの出前なんてどうだろう。もちろん、トッピング(Decorator)は全部のせで頼むよ」

私は苦笑しながらスマホを取り出した。 「わかりました。チーズにカツにほうれん草ですね。……胃袋まで爆発させないでくださいよ、探偵さん」


探偵の調査報告書

容疑(アンチパターン)真実(パターン)証拠(効果)
クラスの爆発(Combinatorial Explosion)。複数の機能の組み合わせをすべて「継承」で表現しようとし、サブクラスが無限に増殖する。Decorator パターン。既存のオブジェクトを、同じインターフェースを持つ「装飾オブジェクト」で包み込み(ラッピング)、機能を外側から追加する。動的な機能の組み合わせ。機能がいくつ増えても、追加するクラスの数は「機能の数」だけで済む(掛け算から足し算への変換)。

推理のステップ

  1. 共通Roleの定義: 本体(Component)と装飾者(Decorator)が同じように振る舞えるよう、共通のインターフェースを定義する。
  2. Decorator基底クラスの作成: 内側に「共通Roleを実装したオブジェクト(inner)」を保持するクラスを作る。
  3. 具体的な装飾の実装: 各機能(ログ、メールなど)ごとにDecoratorのサブクラスを作り、処理の前後に独自のロジックを差し込む。
  4. 実行時の組み立て: クライアント側で、必要な分だけオブジェクトをマトリョーシカのように入れ子にして生成する。

ロックより

ワトソン君。オブジェクト指向を学んだばかりのエンジニアは、なんでもかんでも「継承」で解決しようとして自滅する傾向にある。

だが覚えておきたまえ。「親から受け継ぐ」ことと「道具として持たせる」ことは全く違う。機能の組み合わせに悩んだら、血縁(is-a)を疑い、重ね着(has-a)を試すんだ。君のコードは、もっとオシャレになれるはずだよ。

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