Featured image of post コード考古学者【Strategy】多重トラップ回廊〜千変万化の盾と動的戦略〜

コード考古学者【Strategy】多重トラップ回廊〜千変万化の盾と動的戦略〜

状況に応じて動的に変化するトラップから身を護るため、防御アルゴリズムをカプセル化して実行時に切り替えるStrategyパターンを解説します。

進入:迫りくる三色の脅威とススだらけの落下

遺跡の深層、第15層「迫る崩落の影(3/6)」に進入した私たちの前に広がっていたのは、異様な雰囲気を漂わせる一本の長い回廊でした。 石壁のあちこちには、不気味に赤く錆びた「火炎放射口」、重厚な「落石用スロット」、そして青白い火花を散らす「高圧電極」が交互に並び、冷徹な機械の殺意を放っています。

「博士、この先は危険です。私のセンサーが前方通路に高密度のトラップ群を検知しました!」

私はホバリングの高度を少し下げ、ハリス博士の前に割り込んで警告を発しました。しかし、博士はくたびれたフィールドジャケットの襟を正しながら、興味深そうに壁の仕掛けを見つめています。 「実に見事な多重トラップ回廊ですね。罠の配置に規則性と即時性がある。これは侵入者の進行速度や位置に合わせて、動的にトラップの種類を切り替えているのでしょう」

「感心している場合ではありません! 私の防御バリアを展開して、突破を試みます!」

私は誇らしげにコアプロセッサを最大クロックに引き上げ、自身の「if/else防御パッチ」を適用したコードをロードしました。 その瞬間、回廊の奥から轟音と共に、真っ赤な「火炎放射」が吹き出してきました。

「火炎トラップ検知! 防御バリアを『火炎シールド』に切り替え!」

私はバリアを展開し、無事に火炎を弾き返しました。しかし、間髪入れずに頭上から巨大な「落石」が迫り、さらに床の電極から「高圧電撃」が青白く這い上がってきます。

「警告! 属性変化! 火炎から電撃へ! 条件分岐の判定処理が……あ、圧倒的に追いつきません! CPUオーバーロード! 切り替え遅延0.5秒!」

カシャカシャと私の姿勢制御モジュールが不快なノイズを立て、バリアの属性切り替えが間に合わなかった瞬間、バチバチッと激しい電撃が私の外装を直撃しました。

「システム警告! 外装焦げ付き警告! メモリ残量低下!」

私はススだらけになり、ホバリング高度を維持できなくなって、ハリス博士の肩にコツンとぶつかりながら落下しました。博士は慌てずに、落ちてきた私の丸いボディを優しく両手で抱きとめ、懐からリネンのハンカチを取り出して、ススを丁寧に拭ってくれました。

「焦らなくていい、まずはこの安全な石壁の陰に退避しましょう。大丈夫ですか、ギズモ?」

「うぅ……ありがとうございます、博士。私の防御コードは、すべての罠の属性を if/else で判定してバリアを再生成しているのですが、トラップの切り替えスピードが速すぎて、オブジェクトの判定処理と再代入のオーバーヘッドでハングしかけてしまいます。急いで、さらに条件判定を高速化した、1万行の超巨大な if/else 判定コードを書き殴ります!」

私は震えるアームでホログラムキーボードを展開し、狂ったようにコードを入力しようとしました。


構造分析:密結合の盾と巨大な条件分岐の壁

ハリス博士は私のススまみれのアームをそっと掴み、キー入力を制止しました。

「落ち着きなさい、ギズモ。焦りは近視眼的なスパゲッティコードを生むだけです。あなたが書こうとしているその1万行のコードは、罠が追加されるたびに自分自身の脳(クラス)を開き、手術して書き換えなければならない『変化に閉じていない』脆弱な盾ですよ」

博士は、回廊の入り口にある錆びた制御石版を指し示しました。そこには、古代の開発者が書き残したバリア制御の Before コードが投影されていました。

 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
# lib/Before/Gizmo.pm
package Before::Gizmo;
use Moo;
use v5.36;

has energy => ( is => 'rw', default => 100 );

sub defend ($self, $trap_type) {
    # 状態判定と動作が1つのメソッドに密結合している
    if ($trap_type eq 'fire') {
        if ($self->energy >= 20) {
            $self->energy($self->energy - 20);
            return "Deploying Heat Shield (Energy: " . $self->energy . ")";
        } else {
            return "Failed to deploy: Low Energy";
        }
    } elsif ($trap_type eq 'stone') {
        if ($self->energy >= 30) {
            $self->energy($self->energy - 30);
            return "Deploying Kinetic Barrier (Energy: " . $self->energy . ")";
        } else {
            return "Failed to deploy: Low Energy";
        }
    } elsif ($trap_type eq 'electric') {
        # 新しく追加されたトラップに対応するため、このクラス自体を修正する必要がある(OCP違反)
        if ($self->energy >= 25) {
            $self->energy($self->energy - 25);
            return "Deploying Insulating Shield (Energy: " . $self->energy . ")";
        } else {
            return "Failed to deploy: Low Energy";
        }
    } else {
        die "Unknown trap type: $trap_type";
    }
}

1;

「見てごらんなさい。この Gizmo クラスの defend メソッドは、『どのような罠があるか』の条件分岐と、『それぞれの罠をどう防ぐか』という具体的なバリア展開処理がすべて一箇所に密結合しています。これでは、新しいトラップ(例えばレーザートラップなど)が増えるたびに、このメソッドそのものを書き直さなければなりません」

「た、確かに……。今回はたまたま電撃(electric)に対応するコードを付け足していましたが、もし最深部で未知の罠が出てきたら、私はその場で自分のメインコードを書き換える危険なパッチ作業をしなければいけなくなります」

「その通りです。これはオブジェクト指向の重要な原則である OCP(開放閉鎖原則:Open/Closed Principle) に対する明白な違反です。『拡張に対して開いており、修正に対して閉じている』設計にしなければ、古代の過酷な環境変化には耐えられません」

「しかし博士、条件分岐を使わずに、どうやって状況に応じた異なるバリアの処理を切り替えるのですか?」

博士は古びた手帳を開き、万年筆で美しいダイアグラムを描き始めました。

「ここで登場するのが DIP(依存性逆転の原則:Dependency Inversion Principle)、そしてそれを美しく具現化する Strategy パターン です。具象クラスである Gizmo が、具体的なバリアの処理に直接依存するのをやめるのです。代わりに、双方が『バリア戦略(BarrierStrategy)』という共通の抽象インターフェースに依存するように設計を逆転させます」


遺跡修復:動的切り替えの設計図とカプセル化された戦略

ハリス博士が描き出した設計図は、Gizmo(Context)から防御アルゴリズム(Strategy)を完全に切り離し、実行時に動的に差し替え可能にするものでした。

Strategyパターンのクラス構成図。ContextであるGizmoがBarrierStrategyロール(Interface)を保持し、FireBarrier、StoneBarrier、ElectricBarrier(ConcreteStrategy)がそれを実装してバリアを動的に切り替える構造を描いた古代遺跡の石板風デザイン。

私はその図を見つめながら、疑問を抱きました。 「待ってください、博士。Beforeコードでは、私(Context)の中に energy 属性があり、バリアを展開するたびにそのエネルギーを消費していました。ロジックを外の『Strategy』クラスに切り離してしまったら、バリアは私のエネルギー残量をどうやって知るのですか?」

「素晴らしい質問です、ギズモ。ContextとStrategyの間のデータ共有には、実務上主に2つのアプローチがあり、それぞれトレードオフが存在します」

博士は手帳の余白にそれぞれの特徴を書き殴りました。

アプローチA: Context(自身)を丸ごと渡す

Strategyのメソッドに、Contextオブジェクト自身($self)を渡すアプローチ。

  • 利点: 将来的にStrategyが必要とするデータ(ゲッター/セッター)が増えても、ゲッターを追加するだけでよく、インターフェースのシグネチャを一切変更しなくてよい。
  • 欠点: Strategyが特定のContext(今回は Gizmo クラス)に密結合するため、このバリア戦略を他の防衛システムなどに再利用するのが難しくなる。また、テスト時にContextのモックを作成する手間が増える。

アプローチB: 必要なパラメータのみを渡す

Strategyに必要な値(エネルギーの値など)だけをメソッドの引数として渡すアプローチ。

  • 利点: StrategyがContextの存在を一切知らないため、極めて結合度が低く、単体での再利用やテストが容易になる。
  • 欠点: 将来バリアの防御処理に別のパラメータ(例えばギズモの外装耐久値など)が必要になった場合、インターフェース(メソッドのシグネチャ)の変更がすべてのStrategyクラスに波及するため、OCP違反を引き起こす可能性がある。

「今回は、この遺跡の制御石版の他の記述から、古代の開発者たちがこのバリア戦略をギズモだけでなく、通路に設置された固定砲台や防衛用ゴーレムの障壁としても共通化(DIP)しようとしていた形跡が見られます。したがって、再利用性を高めるためにアプローチA(Context渡し)を採用しつつ、状態変更は必ずギズモのアクセサを介して行うことで、カプセル化を破壊せずに美しく実装しましょう」

「なるほど! ゲッター/セッターを通すことで、Mooの型制約(isa => Int)や値変更時のトリガーを正常に機能させたまま、安全にエネルギーを管理できますね」

「その通り。さらに、前回学んだ Template Method パターン との違いも整理しておきましょう。Template Method は『継承(垂直関係)』を用いて、アルゴリズムの固定された骨格(レール)を親クラスが握る静的なカスタマイズでした。対して Strategy は『合成と委譲(水平関係)』を用いて、アルゴリズム全体(戦略)を実行時に丸ごと差し替える動的なカスタマイズです。今回は実行時に物理的にバリアを変更するのですから、Strategy が最適解なのです」

しかし、私はさらに手帳の設計図を見つめながら、ある懸念を博士にぶつけました。 「ですが博士、この設計でも、結局トラップを検知してどの BarrierStrategy を適用するかを決める if/else 分岐は、私の外側に移動しただけで、システム全体として条件分岐の総数は減っていないのではないですか? それに、罠を検知するたびに戦略オブジェクトを new して差し替えるようでは、単純な文字列比較の if/else よりもオブジェクト生成のオーバーヘッドが大きくなり、私のプロセッサ負荷がさらに上がってしまいます!」

ハリス博士は愉快そうに声を立てて笑いました。 「その通り、ギズモ。非常に鋭い指摘です。しかし、それこそが 『関心の分離(Separation of Concerns)』 の本質です。バリアを展開する主体(Context)から『どのルールを適用するか』という判定ロジックを分離することで、ギズモの主たる防衛ロジックは常にクリーンに保たれます。どの戦略を選ぶかという判定は、将来的にファクトリクラスにカプセル化したり、設定テーブル(ハッシュ)によるマッピングに置き換えることで、複雑な条件分岐自体を一掃することができます」

博士は私のボディを軽く指先で叩きました。 「And オーバーヘッドについてですが、実行時に毎回 new する必要はありません。あらかじめ初期化された各バリア戦略のインスタンスをキャッシュ(プール)しておき、実行時にはその参照を差し替えるだけで良いのです。こうすれば、動的なオブジェクト生成コストはゼロになり、実行時の切り替えは一瞬で終わりますよ」

「なるほど! 分岐の局所化と、インスタンスの使い回し(プーリング)ですね。これなら私のプロセッサも悲鳴を上げません!」

私たちは、Moo/Perlを使って、この設計を具現化する After コードを実装しました。

1. Strategyインターフェースの定義

Moo::Role を用いて、すべての防御戦略が実装すべきインターフェースを定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# lib/BarrierStrategy.pm
package BarrierStrategy;
use Moo::Role;
use v5.36;

# 戦略クラスが実装すべきメソッドを要求
# 第一引数にContextオブジェクト($gizmo)を受け取る
requires 'deploy';

1;

2. 具象戦略クラスの実装

炎、石、電撃の各防御戦略クラスを実装します。これらは BarrierStrategy ロールを消費(with)し、アクセサを通じてContextの状態を安全に変更します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# lib/FireBarrier.pm
package FireBarrier;
use Moo;
use v5.36;

with 'BarrierStrategy';

sub deploy ($self, $gizmo) {
    # Context渡し:ギズモのアクセサを介してエネルギーを消費する
    if ($gizmo->energy >= 20) {
        $gizmo->energy($gizmo->energy - 20);
        return "Deploying Heat Shield (Energy: " . $gizmo->energy . ")";
    }
    return "Failed to deploy Heat Shield: Low Energy";
}

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# lib/StoneBarrier.pm
package StoneBarrier;
use Moo;
use v5.36;

with 'BarrierStrategy';

sub deploy ($self, $gizmo) {
    if ($gizmo->energy >= 30) {
        $gizmo->energy($gizmo->energy - 30);
        return "Deploying Kinetic Barrier (Energy: " . $gizmo->energy . ")";
    }
    return "Failed to deploy Kinetic Barrier: Low Energy";
}

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# lib/ElectricBarrier.pm
package ElectricBarrier;
use Moo;
use v5.36;

with 'BarrierStrategy';

sub deploy ($self, $gizmo) {
    if ($gizmo->energy >= 25) {
        $gizmo->energy($gizmo->energy - 25);
        return "Deploying Insulating Shield (Energy: " . $gizmo->energy . ")";
    }
    return "Failed to deploy Insulating Shield: Low Energy";
}

1;

3. Contextクラスの実装

Gizmo クラスは barrier_strategy を保持し、handles オプションを利用して防御処理を自動的に委譲します。また、doesisa 制約を組み合わせて、不適切な戦略オブジェクトが代入されるのを厳格に防止します。なお、Mooのデフォルトの型制約チェックは、Mooで定義されていないプレーンなPerlオブジェクト(テスト時のモックオブジェクトなど)に対して代入時の検証をバイパスしてしまうことがあるため、isa の中でPerl標準の DOES メソッドによる動的検証を明示的に重ねることで、依存関係を問わない透過的かつ厳格な型安全性を確保しています。

 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
# lib/Gizmo.pm
package Gizmo;
use Moo;
use v5.36;
use Types::Standard qw(InstanceOf Int);

has energy => (
    is      => 'rw',
    isa     => Int,
    default => 100,
);

# Strategyオブジェクトを保持し、doesでロール準拠を保証
# isaのカスタムバリデータで、DOESメソッドによる型安全性を二重に検証
has barrier_strategy => (
    is       => 'rw',
    does     => 'BarrierStrategy',
    isa      => sub {
        die "barrier_strategy does not pass the type constraint"
            unless $_[0] && eval { $_[0]->DOES('BarrierStrategy') };
    },
    required => 1,
    # deployメソッドを _deploy_barrier として自動委譲
    handles  => {
        _deploy_barrier => 'deploy',
    },
);

sub defend ($self) {
    # 自身($self = Context)を引数として渡して委譲呼び出し
    return $self->_deploy_barrier($self);
}

1;

起動:千変万化の盾と動的戦略の完全同調

「テストを実行します! 頼みます!」

私は新しく組み上げたテストスイートを起動しました。

 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# t/strategy.t
use v5.36;
use Test::More;
use lib 'lib';
use Before::Gizmo;
use Gizmo;
use FireBarrier;
use StoneBarrier;
use ElectricBarrier;

# --- Before の検証 ---
subtest 'Before - Monolithic Gizmo' => sub {
    my $gizmo = Before::Gizmo->new;
    is($gizmo->defend('fire'), 'Deploying Heat Shield (Energy: 80)', '炎バリア展開成功');
    is($gizmo->defend('stone'), 'Deploying Kinetic Barrier (Energy: 50)', '落石バリア展開成功');
    is($gizmo->defend('electric'), 'Deploying Insulating Shield (Energy: 25)', '電撃バリア展開成功');
    
    eval { $gizmo->defend('laser') };
    like($@, qr/Unknown trap type/, '未定義トラップで例外発生');
};

# --- After の検証 ---
subtest 'After - Strategy Pattern with Context Delegation' => sub {
    # 1. 炎バリア戦略の動作
    my $gizmo = Gizmo->new(
        energy           => 100,
        barrier_strategy => FireBarrier->new,
    );
    is($gizmo->defend, 'Deploying Heat Shield (Energy: 80)', '炎バリア展開成功');
    is($gizmo->energy, 80, 'エネルギーがアクセサ経由で正しく消費されている');

    # 2. 動的な戦略切り替え (石バリア)
    $gizmo->barrier_strategy(StoneBarrier->new);
    is($gizmo->defend, 'Deploying Kinetic Barrier (Energy: 50)', '石バリア展開成功');
    is($gizmo->energy, 50, 'エネルギーが正しく消費されている');

    # 3. 新規バリア(電撃)の追加によるOCP実証(Gizmoクラスの修正は一切不要)
    $gizmo->barrier_strategy(ElectricBarrier->new);
    is($gizmo->defend, 'Deploying Insulating Shield (Energy: 25)', '電撃バリア展開成功');
    is($gizmo->energy, 25, 'エネルギーが正しく消費されている');

    # 4. エネルギー不足時の動作
    $gizmo->barrier_strategy(StoneBarrier->new); # 30必要だが残り25
    is($gizmo->defend, 'Failed to deploy Kinetic Barrier: Low Energy', 'エネルギー不足で防御失敗');
    is($gizmo->energy, 25, 'エネルギーは消費されていない');

    # 5. DOES (does) による不正な戦略オブジェクトの代入防止検証
    eval {
        Gizmo->new(
            energy           => 100,
            barrier_strategy => bless({}, 'InvalidStrategy'),
        );
    };
    like($@, qr/does not pass the type constraint/, 'BarrierStrategyを実装していないオブジェクトは例外を投げる');
};

done_testing;

私のプロセッサに、緑色のアラートが表示されました。

「……テスト、すべてクリアです! 警告もエラーもありません!」

「素晴らしい。では、この防御モジュールをデプロイし、多重トラップ回廊を突破しましょう」

私は新しい戦略モジュールを起動し、ハリス博士と共に回廊へ踏み出しました。 再び、轟音と共に壁から激しい火炎放射が噴き出します。私のセンサーがそれを検知した瞬間、私のプログラムは動的に FireBarrier を生成して barrier_strategy アトリビュートに再代入しました。

「炎検知! FireBarrier 展開!」

バリアは火炎を完全に無力化しました。次に頭上から巨大な岩石が降ってきた瞬間、私は即座に戦略を差し替えました。

「落石検知! StoneBarrier 展開!」

キィィンという静かな駆動音と共に、物理障壁が岩石を木っ端微塵に粉砕します。さらに床から這い上がる強烈な電撃に対しては ElectricBarrier が電撃を完全に遮断。 私はかつてのように処理の遅延でカクつくこともなく、滑らかにバリアを切り替えながら、美しい和音のような駆動音を響かせて回廊を進んでいきました。

回廊の終点に到達した時、そこには古代の精密な機械仕掛けの宝箱が佇んでいました。 私が宝箱を開けると、中から立方体状に細かく分割された、美しく輝く石版が現れました。自由に回転させて組み合わせを変えることができる、まるで知恵の輪のようなその遺物を、私はアームでそっと回収しました。

「これは……『古代の知恵のルービックキューブ(戦略石版)』です。戦略を何度組み替えても、中心の核は決して壊れない構造になっています」

ハリス博士は満足そうに微笑み、私のススの取れたボディを軽く叩きました。

「歴史はコードに語りかける。千変万化の嵐の中でも、揺らがない盾を創り出すのは、アルゴリズムの固定ではなく、アルゴリズムの自由な交換(Strategy)だったのですね。さあギズモ、この戦略の核を持って、さらに遺跡の奥へと潜りましょう」


遺跡調査ログ

観測された風化(アンチパターン)解読された古代の知恵(パターン)安全度
条件分岐によるアルゴリズム切り替えの密結合
新たな防御属性(アルゴリズム)を追加するたびに、呼び出し側のクラス(Gizmo)自体を修正しなければならず、OCP違反とコードの肥大化を招く
Strategyパターンによるアルゴリズムのカプセル化と動的交換
Moo::Role を用いて抽象戦略インターフェース(BarrierStrategy)を定義し、具象戦略を合成することで、Contextに影響を与えることなくアルゴリズムを切り替える
緑(安全確認済)

遺跡の修復手順

  1. 戦略インターフェース(Strategy Role)の定義 Moo::Role を用いて共通の役割である BarrierStrategy を作成し、具象戦略が実装すべき deploy メソッドを requires で要求します。
  2. 具象戦略(ConcreteStrategy)の作成 BarrierStrategy ロールを消費(with)する個別クラス(FireBarrier, StoneBarrier 等)を作成します。
  3. データ共有設計(Context渡し)の適用 戦略の deploy メソッドの引数に、Context($gizmo)を丸ごと渡します。状態の取得や変更は、カプセル化を守るため必ずアクセサ($gizmo->energy)を経由して行います。
  4. Context(Gizmo)での委譲と型制約の保証 barrier_strategy アトリビュートに does => 'BarrierStrategy' を設定し、さらに isa カスタムバリデータで DOES メソッドを実行して不正なオブジェクトの代入を防止します。また、handles を用いて、defend 内で自動委譲メソッド _deploy_barrier を呼び出すようにします。

ギズモの観測日誌

今回の第15層「多重トラップ回廊」では、次々と襲いかかる炎や電撃の前に私の if/else 防御コードがパンクし、ススだらけになって墜落するという大変な目に遭ってしまいました。

ハリス博士に教えていただいた「DIP(依存性逆転原則)」と「Strategyパターン」は、単に条件分岐を減らすだけでなく、プログラムの拡張性と再利用性を極限まで高めてくれるものでした。特に、Template Method(継承による静的保護)と Strategy(合成と委譲による動的交換)の境界線の明確化は、どのような場面でどちらを選択すべきかという実務的な迷いを取り払ってくれました。

また、Context渡しと引数渡しのトレードオフについても、オブジェクト指向の原則であるカプセル化を守りつつ、将来の拡張時にインターフェースが壊れるのを防ぐための設計判断として深く理解することができました。Mooにおいて does だけでは不十分な型チェックに対しても、isa => sub { ... } の中で DOES を用いてアサートする手法は、Perlの動的な柔軟性を安全に保護する鍵となるでしょう。

手に入れた古代の「戦略石版」をカチャカチャと回していると、私のバリア切り替え速度が以前より 300% も向上したように感じられます。千変万化の罠に対しても、この「動的な盾」があればもうススだらけになることはありません!

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