Featured image of post 【Perl/Moo】コード考古学者ハリスの冒険【Bridge】多重制御の石門〜直交する拡張と機能の分離〜

【Perl/Moo】コード考古学者ハリスの冒険【Bridge】多重制御の石門〜直交する拡張と機能の分離〜

バベルのシステム中層第7層での予期せぬ水門の暴走。Mooを用いた多重継承・ロール適用による初期化順序の崩壊を、Bridgeパターンによる抽象と実装の分離で修復する手法を解説。

進入

中層の入り口である第7層「絡み合う暗路」は、重厚な石組みの壁から古い機械音が絶え間なく響く、巨大な水処理制御室でした。

しかし現在、この部屋は知的探索とは程遠い、極限のサバイバル空間へと変貌しています。すでに私たちの足元には、冷たい水がひたひたと満ち始めていました。

「ハリス博士、本当に、本当に申し訳ありません!すべては私の注意散漫と、慢心が招いた結果です!」

私はホバリングの高度を少し上げ、センサーの赤い光を激しく明滅させながら叫びました。

「いやはや、謝る必要はありませんぞ、ギズモ。それにしてもこの水位の上昇スピード、実にダイナミックですな」

ハリス博士は膝まで水に浸かりながら、まったく動じる様子もなく、目の前の真鍮製コントロールパネルをうっとりと見つめています。

「呑気すぎます!前回の古代コンソールで検出された開発者の署名『L.O.V.E.L.A.C.E.』。あのイニシャルが私の識別コードと一致したことで、私の脳内メモリがバックグラウンドで重い解析処理を走らせてしまい、CPU使用率が100%近くに達していたのです。そのせいで注意力が散漫になっていました」

「ふむ、なるほど。しかしギズモ、私のジャケットのポケットに入っている『真鍮ピンのお土産』(前回のアダプター)が錆びてしまいませんかね? 実に美しい加工だったので少々心配ですな」

「お土産の心配をしている場合ですか! あと五分でこの部屋は完全に水没するのです! それに、私がやらかしたのはメモリ負荷のせいだけではありません。前回のAdapterパターンの成功で、私は『どんな仕様の不一致も、後からラッパー(Adapter)を作って無理やり繋れば何とでもなる』と傲慢になっていました。だから、壊れていた『ボタン式パネル』を、博士が好む『真鍮のレバー式パネル』へその場しのぎのラッパーを介して無理やり換装してしまったのです。そうしたら、接続した瞬間に水門の制御シークエンスが狂い、バルブが全開のまま固定されてしまいました!」

「アタカマの遺跡の毒矢トラップや、第4層の防壁ゲートに比べれば、この冷たい水はただの歓迎のシャワーのようなものですな。風化に立ち向かう者への恵みの雨です」

「流石に物理的な水没まで歓迎と捉えて美化しないでください! 溺れ死ぬのが先か、ショートするのが先かの瀬戸際です!」

博士は愛用のルーペを取り出すと、水中に半分沈みかけた古い制御基板の隙間から覗く、古代の配線コードを観察し始めました。

「ギズモ、君が書いた換装前の Before コードを見せていただけますか?焦る必要はありません。歴史の謎を解き明かす鍵は、常に崩落の危機の中に眠っているものですからな」

「歴史ではなく、私たちの命が眠ってしまいます! データを投影します!」


構造分析

私のホログラムプロジェクターから、水面に反射するBeforeコードが映し出されました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# lib/ControlPanelBase.pm (操作盤の基底クラス)
package ControlPanelBase;
use Moo;
use v5.36;

sub initialize ($self) {
    # 制御パネルの共通基盤初期化
    return "Base control panel initialized.";
}

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# lib/WaterGateRole.pm (水門の制御ロール)
package WaterGateRole;
use Moo::Role;
use v5.36;

# ControlPanelBaseのinitializeと名前が衝突している!
sub initialize ($self) {
    return "Hydraulic valve initialized.";
}

sub open ($self) {
    return "Opening water gate.";
}

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# lib/LeverWaterGateControlPanel.pm (Before: 多重継承とロール適用の闇)
package LeverWaterGateControlPanel;
use Moo;
use v5.36;

extends 'ControlPanelBase';
with 'WaterGateRole';

sub pull_lever ($self) {
    # initializeが衝突している。Mooのルールにより、ロール側のメソッドが
    # 継承元の親クラスのメソッドをサイレントに上書きして適用される。
    my $status = $self->initialize();
    return "Lever pulled. " . $self->open() . " Status: $status";
}

1;

「ハリス博士! 私は操作方法(レバー式)と、扉(水門)の振る舞いを合体させるために、基底クラスを extends しつつ、水門ロールを with しました。PerlのMooなら、これで両方の機能を備えた新しい操作盤が作れると思ったのです!」

水位はすでに、博士の腰のあたりまで達していました。冷たい水が機械の電子回路を脅かしています。

「システム警告!水位上昇、限界値まであと60センチ! 博士、なぜこれで暴走するのですか!?」

「ふむ、ギズモ。君はMooのロールの挙動を誤解していたようですな。Mooにおいて、クラス(または継承した親クラス)とロールで同名のメソッド(今回で言えば initialize)が存在する場合、ロールのメソッドが優先され、継承元のメソッドをサイレントに上書きして適用してしまうのだよ」

「えっ? ということは……」

「その通り。君が pull_lever を実行したとき、ロール側の initialize(油圧バルブの初期化)は動いたが、親クラス ControlPanelBaseinitialize(操作盤基盤の初期化)は完全にスキップされてしまったのだ。基盤が初期化されずに無理やりバルブを開こうとしたため、制御コアがパニックを起こし、全開のままフリーズした。これが水門暴走の正体ですな」

「システム警告! 水位が胸に達しました! 博士、話が長いです!多重継承(@ISA の羅列)で回避すればいいのではないですか!?」

「それこそが奈落の罠ですぞ」博士は首を振り、水を跳ね上げながら語ります。「多重継承に依存すると、今度はPerlのMRO(Method Resolution Order: C3アルゴリズムによる解決順序)が複雑に混濁し、どの順番で BUILD(初期化フック)が呼ばれるか制御不能になる。さらに、操作方法(ボタン、レバー、ダイヤル)が $M$ 個あり、扉の種類(水門、石門、鉄格子)が $N$ 個あるとしよう。これらを継承だけで組み合わせようとすると、クラス数が $M \times N$ に膨れ上がる『クラス爆発』が起きるのだ」

「では、Adapterパターンで……!」

「いいえ。Adapterは『既に存在する非互換なもの(古代端子)』を繋ぐための一時的な事後対応。対して、私たちが今立ち向かうべきは、これから『操作方法(機能)』も『扉の種類(実装)』も両方が独立して増えていく、直交する2つの次元だ。これらを最初から分離して結合する美しい架け橋――すなわち『Bridgeパターン』こそが、この風化した密結合を解く賢者の知恵なのですな」

「ちなみに、実行時にアルゴリズムを切り替える『Strategyパターン』は『振る舞い』の分離ですが、今回は構造そのものを分離する『Bridge』が最適です! 博士、早く設計図を!」


遺跡修復

「おっと、水が手帳に染みる前に描き上げねばなりませんな」

ハリス博士は胸ポケットから万年筆を取り出し、濡れた手帳の白いページに力強くクラス設計図をスケッチしました。

手書き設計図

古代遺跡の石板風に描かれたBridgeパターンのクラス図。左側には機能の階層(抽象)として、上位に『ControlPanel』、それを継承する下位に『LeverControlPanel』が配置されています。右側には実装の階層(具象)として、上位にロールである『Gate』、それを消費する下位に『WaterGate』が配置されています。左上の『ControlPanel』から右上の『Gate』に対して、『does(委譲)』を示す青く光る水平な線が繋がっており、操作盤と扉の動作が分離・架橋されている構造を示しています。

「機能の階層(操作盤:Abstraction)と、実装の階層(扉:Implementor)を完全に分離する。操作盤は扉の具体的な中身を知らず、ただ『Gate』ロールを持つオブジェクトを委譲(コンポジション)によって保持するのだ。これでクラス数は $M \times N$ から $M + N$ に削減され、互いに影響を与えることなく拡張できるようになる」

「素晴らしいです! 水が顎に届く前に、この設計図をコードに焼き付けます!」

私はマニピュレータをキーボード代わりにホログラム投影へ走らせ、Afterコードを組み立てました。

1
2
3
4
5
6
7
8
# lib/Gate.pm (Implementorのインターフェースを表すRole)
package Gate;
use Moo::Role;
use v5.36;

requires qw(initialize_device open_device);

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# lib/WaterGate.pm (ConcreteImplementor: 具体的な水門の実装)
package WaterGate;
use Moo;
use v5.36;
with 'Gate';

has status => (
    is      => 'rw',
    default => sub { 'uninitialized' },
);

sub initialize_device ($self) {
    $self->status('Hydraulic valve initialized.');
    return $self->status;
}

sub open_device ($self) {
    return "Opening water gate.";
}

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
# lib/ControlPanel.pm (Abstraction: 操作盤の抽象親クラス)
package ControlPanel;
use Moo;
use v5.36;

# 扉の実装オブジェクトをコンポジション(委譲)で保持する
has gate => (
    is       => 'ro',
    does     => 'Gate',
    required => 1,
);

has base_status => (
    is      => 'rw',
    default => sub { 'uninitialized' },
);

sub BUILD ($self, $args = {}) {
    # 自クラス(操作盤)の初期化
    $self->base_status("Base control panel initialized.");
    # 委譲先の初期化を安全に呼び出す(メソッド衝突は起きない)
    $self->gate->initialize_device();
}

1;

依存性注入(DI)と宣言的初期化の優位性

「ハリス博士、このAfterコードで、操作盤のインスタンスを作る際に $panel = LeverControlPanel->new( gate => $water_gate ) のように gate オブジェクトを外から渡していますね。これはなぜですか?」

「それこそが**依存性注入(Dependency Injection: DI)**の妙技だ。操作盤自身が具体的な『水門』クラスを内部で new するのではなく、インターフェース(Gate ロール)を満たすオブジェクトを外部からコンストラクタのハッシュ引数で注入する。これにより、操作盤は具体的な扉に一切依存しなくなる。テスト時にはモックオブジェクトを注入することも容易になり、クラス間の結合度が極限まで下がるのだよ」

「なるほど! 委譲を支えるのはDIなのですね。でも、BUILD メソッドの中で手続き的に $self->gate->initialize_device() を呼んでいるのは、少しオブジェクト指向らしくない気が……」

「鋭い指摘ですな、ギズモ。今回は水没というタイムリミットがあったため、手動で手続き的に呼び出す BUILD を用いたが、Mooのベストプラクティスとしては、手続き的な BUILD を極力排除すべきだ。属性の defaultbuilder を用いて、かつ lazy => 1(遅延評価)を設定すれば、値が必要になったタイミングで依存オブジェクトが自動的かつ宣言的に初期化される。オブジェクトの不完全な状態を防ぎ、より堅牢なオブジェクトライフサイクルを構築するなら、そちらの方が優位なのだ。今回はまずこのシンプルなBridgeで水門を閉め、乾いた場所で宣言的初期化へとリファクタリングしましょう」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# lib/LeverControlPanel.pm (RefinedAbstraction: 具体的な操作盤)
package LeverControlPanel;
use Moo;
use v5.36;
extends 'ControlPanel';

sub pull_lever ($self) {
    # 保持している扉(Gate)のデバイスに処理を委譲する
    my $action = $self->gate->open_device();
    return "Lever pulled. Action: $action " .
           "Base: " . $self->base_status . " " .
           "Gate: " . $self->gate->status;
}

1;

ゲート開通

「コード修復完了! 実行します!」

水位が私たちの首に達し、ホバリング用の電子ローターがショートする寸前、書き換えたプログラムが稼働しました。

私が換装した真鍮のレバーを力強く引き下げると、新しいプログラムに基づき、操作盤の基底初期化と水門の油圧初期化が衝突することなく安全に順次実行され、バルブ制御コマンドが正しく水門へ委譲されました。

ゴゴゴゴゴ……!

重々しい駆動音とともに、全開になっていた水門がゆっくりと閉じていきます。同時に、部屋の隅にある排水バルブが開き、溜まっていた水が勢いよく渦を巻いて引いていきました。

「ふう……九死に一生を得ましたな」

ハリス博士はすっかり濡れて重くなったフィールドジャケットを脱ぎ、固く絞りながら楽しそうに笑いました。

「まったくです! センサーが完全に錆びるところでした。ですが、これで機能と実装の結合が美しく分離されました。今後、どのような古代の扉が現れても、操作盤の設計を一切汚すことなく対応できます!」

「ええ。木と縄という異なる役割が美しく調和して架けられる『吊り橋(Bridge)』のように、完璧な役割分担ですな」

博士は濡れた手帳をそっと閉じ、愛おしそうに真鍮レバーを眺めました。

私の内部データベースには、修復された水処理モジュールの全容マップが綺麗に復元され、さらに奥へと続く「絡み合う暗路」の安全なルートを照らし出していました。


遺跡調査ログ

観測された風化(アンチパターン)解読された古代の知恵(パターン)安全度
多重継承とロール適用の混濁 (同名メソッドの衝突・初期化順序の崩壊による水門の暴走)Bridge パターン (機能と実装の階層を分離し、委譲によって結合する)🟢 探索ルート安全確保 (テスト通過)

遺跡の修復手順

  1. 直交する変動軸の特定: 拡張される可能性のある「機能(Abstraction:操作方法)」と「実装(Implementor:動作対象)」の2つの軸を明確に分離する
  2. Implementorインターフェースの定義: MooのRole(例: Gate)を用いて、実装クラスが共通で提供すべき低レベル操作を requires で定義する
  3. Abstraction基底クラスの作成: Mooクラス(例: ControlPanel)を作成し、has gate 属性によってImplementorインターフェースを持つオブジェクトをコンポジション(委譲)で保持する
  4. 具体的な拡張クラスの構築: Abstractionを継承したクラス(例: LeverControlPanel)や、Implementorロールを消費するクラス(例: WaterGate)をそれぞれ独立して作成し、委譲メソッドを呼び出す

ギズモの観測日誌

今回の水没ピンチは、私の前回のAdapterパターンの成功に対する「万能感(認知の歪み)」が引き起こしたものでした。どんな非互換もその場しのぎのラッパーで解決しようとすると、結果として多重継承の闇や初期化順序の崩壊という大きなデグレを引き起こしてしまいます。

ハリス博士が描き出した「Bridge(架け橋)」の設計図は、継承の呪縛を解き放ち、コンポジション(委譲)によるカプセル化がいかに強力であるかを教えてくれました。依存性注入(DI)によってテスト容易性が高まり、コードの美しさは格段に向上しています。博士からアドバイスされた「BUILD による手続き的初期化を排し、lazybuilder による宣言的初期化へ移行する」というMooのさらなる高みについては、次の探索フェーズの宿題とします。排水が無事に終わり、博士は「濡れたフィールドジャケットが味わい深くなった」と喜んでいます。案内AIとしての私の論理構造も、より強固なものにアップデートされました。

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