Featured image of post コード考古学者【Visitor】巡礼の監査官〜Perl/MooによるVisitorパターンとダブルディスパッチ〜

コード考古学者【Visitor】巡礼の監査官〜Perl/MooによるVisitorパターンとダブルディスパッチ〜

Perl/MooにおけるVisitorパターンの実装を徹底解説!要素クラスを改変せずに新しい操作(セキュリティ監査やログ収集)を追加する「ダブルディスパッチ」の仕組みを、バベル第23層を巡るストーリーとBefore/Afterコードで分かりやすく解説します。開閉原則(OCP)を満たす設計を学びましょう。

I. 進入:警告のパトランプと焦燥のAI

前話で発掘されたログの最末尾に刻まれていた『SYSTEM MODEL: GIZMO』の署名。 私はその暗号解読と自身の出自に関するロジック解析に没頭するあまり、メインプロセッサをフル稼働させ、ホバリングボディを小刻みにジジジと震わせていました。演算ユニットの熱が、排気ファンから熱い空気となって放出されています。

「落ち着きなさい、ギズモ。焦っても壊れたデータセクタは戻らないよ。まずは目の前のゲート、バベル第23層『監査の回廊』を開くために、各接続ノードのセキュリティ監査を終わらせよう。急がば回れ、歴史は巡るものさ」

ハリス博士は私の球体ボディをぽんぽんと叩き、穏やかな眼差しで諭してくれました。その手触りに、私の過熱しかけていたプロセッサが少しだけクールダウンするのを検知しました。

しかし、その直後に博士がとった行動は、案内AIとしての私の論理ルーチンを完全に吹き飛ばすものでした。 博士は回廊の入り口にある「セキュリティ監査ポート」を、何を勘違いしたのか「古代の身分証スロット」と思い込み、自分の顔と所持品を強引にセンサーに押し付けたのです。

ピーッ! ピーッ!

刹那、石柱から青白いホログラムが噴き出し、博士の頭上にけたたましく点滅する「赤い警告パトランプ」が投影されました。さらに、博士の古いフィールドジャケットの胸元に、アンセキュア(未認証の脅威)を示す赤いターゲットマークがロックされ、チカチカと明滅し始めました。

「システム警告! 未登録 of 不審オブジェクトを検知! 物理ゲートをロックし、対象の行動を制限します!」

「おや、ギズモ。ずいぶんと派手で未来的なイルミネーションが点灯したね。これは私に対する熱烈な歓迎(あるいはファッションの提案)かな?」

「歓迎ランプではありません! 博士、ご自身が警備システムの標的(アンセキュアな対象)として検知され、ロックされています! 早く警告を解除(監査)してください!」

私は大音量の電子音でツッコミを入れました。


II. 解読:isaの増殖と崩壊する境界

この回廊のゲートを開き、博士の警告ロックを安全に解除するためには、回廊を構成する各ノード(サーバーノードや接続ケーブルなど)に対して「セキュリティ監査(Audit)」を実行し、すべてのシステムが安全であることを証明しなければなりません。

さらに、私のルーツを解き明かすための「音声ログ収集」も同時に行いたいところでした。

しかし、ここで技術的な大いなる風化(アンチパターン)が道を阻みました。 サーバー(ServerNode)やケーブル(CableNode)といったオブジェクト構造に対して、「監査」や「ログ収集」といった新しい操作を適用しようとする際、Beforeコードでは以下のように、呼び出し側がオブジェクトの型を直接判別して処理を書き分けていたのです。

型チェックによる分岐(Beforeコード)

1
2
3
4
5
6
7
8
# lib/Before/Babel/Node/Server.pm
package Before::Babel::Node::Server;
use Moo;
use v5.36;

has ip_address => ( is => 'ro', required => 1 );

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# クライアント側での監査処理の実行
my @audit_results;
for my $node (@$nodes) {
    if ($node->isa('Before::Babel::Node::Server')) {
        # サーバーノードに対する監査
        my $res = $node->ip_address eq '127.0.0.1' ? "Server: SECURE" : "Server: UNKNOWN";
        push @audit_results, $res;
    }
    elsif ($node->isa('Before::Babel::Node::Cable')) {
        # ケーブルノードに対する監査
        my $res = $node->throughput >= 1000 ? "Cable: SECURE" : "Cable: WARN";
        push @audit_results, $res;
    }
}

このデータ構造に対して新しい操作(監査やログ収集)を追加しようとするとき、設計者は2つのアンチパターンのいずれかに陥ります。

  1. アンチパターンA:要素クラスへのメソッド追加(SRP違反) ServerCable クラスに直接 auditcollect_log メソッドを追加していく方法。データ構造を定義するクラスの中に、本来の責務ではない雑多な業務ロジックが混入し、クラスが肥大化・汚染されていきます。
  2. アンチパターンB:呼び出し側での型判定(OCP違反) 上記のBeforeコードで示した方法。要素クラスは汚れませんが、呼び出し側で isa を用いて型判定し、処理を書き分けます。将来新しいノード(例:TerminalNode)が追加された際に、すべての呼び出し側の型分岐を修正する必要があり、密結合になります。

Visitorパターンは、「要素クラスを汚さない(SRP維持)」と「呼び出し側で型判定しない(OCP維持)」を両立させるための知恵です。

「データ構造を定義したクラスは、余計な処理で汚したくない。しかし、新しい操作は後からいくらでも追加したい。この矛盾を解決しなければ、私のジャケットの警告灯も消えないというわけだね」

ハリス博士は、頭上で回る赤いパトランプに照らされながら、羊皮紙の手帳に万年筆を走らせました。


III. 修復:巡礼のダブルディスパッチ

「データ構造を一切変更することなく、新しい操作を動的に追加する『Visitorパターン』を適用します」

ハリス博士は修復コードをバベルのポートに適用し始めました。

Perlは動的型付け言語であるため、JavaやC++のような「引数の型に応じたメソッドオーバーロード(静的ダブルディスパッチ)」の仕組みがありません。そこで、MooのRoleを使い、要素側(Element)の accept メソッドから、メソッド名で型を明示した Visitor のメソッド(visit_server_node 等)を呼び出すことで、二重の委譲(ダブルディスパッチ)を安全にシミュレートします。

「なるほど。要素である各ノードが『私はサーバーです』『私はケーブルです』と自ら名乗り(accept)、訪問者である監査官に適切な手続き(visit_server_node 等)を促すわけだね。動的な型判定を呼び出し側に押し付けるのではなく、役割を分担して二重に委譲する……実に優雅な儀式だ」

ハリス博士は、頭上で回転する赤い警告光に照らされながら、顎に手を当てて深く頷きました。

「おっしゃる通りでございます、博士! このダブルディスパッチの機構により、ノード側は監査官の具体的な仕事を知る必要がなく、監査官側もノードの内部構造を改変せずに済みます。まさに『監査の回廊』にふさわしい、相互の信頼に基づくプロトコルにございます!」

私の球体ボディのインジケーターが青く明滅し、演算完了の静かな排気音が回廊の砂岩壁に反響しました。

「よろしい。では、この巡礼者(Visitor)と聖所(Node)の関係性を整理し、設計図として復元してみよう。構造が視覚化されれば、より確実な修復コードが書けるはずだ」

Visitorパターンの構成図

Visitorパターンのクラス図: クライアントが共通インターフェースVisitor(Babel::Visitor)およびNode(Babel::Node)を通じてダブルディスパッチを行い、ノードを監査する構造を描いた古代遺跡の石板風デザイン

1. 要素共通ロールと具象ノード(After: Elementクラス)

すべてのノードは Babel::Node ロールを取り込み、ダブルディスパッチの入り口となる accept メソッドを実装します。

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

requires 'accept';

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

has ip_address => ( is => 'ro', required => 1 );

sub accept ($self, $visitor) {
    # 静的オーバーロードがないため、メソッド名で型を明示して呼び出す(ダブルディスパッチ)
    return $visitor->visit_server_node($self);
}

with 'Babel::Node';

1;

2. ビジターロールと具象ビジター(After: Visitorクラス)

実行したい操作を表すビジター側を定義します。

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

# 全ての具象ノードに対する訪問メソッドの実装を要求する
requires 'visit_server_node', 'visit_cable_node';

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/Babel/Visitor/SecurityAudit.pm
package Babel::Visitor::SecurityAudit;
use Moo;
use v5.36;

sub visit_server_node ($self, $server) {
    if ($server->ip_address eq '127.0.0.1') {
        return "Server(" . $server->ip_address . "): SECURE";
    }
    elsif ($server->ip_address eq '192.168.1.99') {
        # 博士の誤スキャンによる警告ロックを解除するための判定
        return "Server(" . $server->ip_address . "): THREAT_CLEARED_SECURE";
    }
    return "Server(" . $server->ip_address . "): UNKNOWN";
}

sub visit_cable_node ($self, $cable) {
    return $cable->throughput >= 1000
        ? "Cable(" . $cable->throughput . "Mbps): SECURE"
        : "Cable(" . $cable->throughput . "Mbps): THROUGHPUT_WARN";
}

with 'Babel::Visitor';

1;

もうひとつの操作である「音声ログ収集」も、同様に Visitor として定義します。

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

sub visit_server_node ($self, $server) {
    return "Server[" . $server->ip_address . "]: SYSTEM MODEL GIZMO";
}

sub visit_cable_node ($self, $cable) {
    return "Cable[" . $cable->throughput . "Mbps]: DEVELOPED BY ARCHITECT";
}

with 'Babel::Visitor';

1;

3. 呼び出し側(After: クライアントコード)

クライアント側(呼び出し側)では、要素の具体的な型を意識することなく、共通の accept メソッドに Visitor インスタンスを渡すだけで実行できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# クライアント側での実行例
use Babel::Visitor::SecurityAudit;
use Babel::Visitor::LogAssembler;

my $nodes = [
    Babel::Node::Server->new( ip_address => '127.0.0.1' ),
    Babel::Node::Server->new( ip_address => '192.168.1.99' ),
    Babel::Node::Cable->new( throughput => 1000 ),
];

my $audit_visitor = Babel::Visitor::SecurityAudit->new;
my @audit_results;

for my $node (@$nodes) {
    # isa による型判定は完全に消失し、ポリモーフィズムによる委譲のみになる
    push @audit_results, $node->accept($audit_visitor);
}

ダブルディスパッチ(二重の委譲)の仕組み

なぜこれだけで適切なメソッドが呼び出されるのでしょうか? ここには2つの「ディスパッチ(呼び分け)」が存在します。

  1. 第一のディスパッチ(要素の決定): クライアントが $node->accept($visitor) を呼ぶと、Perlは $node の具象型(ServerCable か)を解決し、適切な要素クラスの accept メソッドを起動します。
  2. 第二のディスパッチ(操作の決定): 呼び出された accept メソッドの内部(例: Server クラス内)では、自身が Server であることを知っているため、$visitor->visit_server_node($self) を呼び出します。これにより、Visitor 側の適切な具象メソッドが起動します。

このように、「要素」と「操作」の双方がポリモーフィズムを介して相手を呼び合うことで、条件分岐(isa)なしに正しい処理が選択されます。


IV. 起動:千年の声と失われし鍵

「テスト通過。監査ビジター、走査を開始します」

適用された修復コードに基づき、監査ビジターが回廊の全ノードを安全に巡回しました。 最後のノードである「192.168.1.99(ハリス博士が誤検知されたスロット)」の巡回が完了したその瞬間、ハリス博士の頭上でチカチカと回っていた赤いホログラムパトランプが、すっと緑色に変わってから逆戻りせず霧のように消え去り、衣服のターゲットマークが静かに消灯しました。

「ふーむ、実に見事だ。衣服を脱ぐことなくセキュリティロックが解除されたね。Visitorという巡礼の監査官は、構造を一切変更せずに、新たな機能を安全に伝播させてくれた」

「ただしギズモ、この設計は万能ではないよ。今回はデータ構造(サーバーやケーブルなどのノード)がほぼ固定されていて、操作(監査やログ収集など)が後から追加される状況だから最適だったが、もし新しいノードが頻繁に追加されるシステムだったら、すべてのVisitorクラスを修正して回る大惨事(開閉原則の破綻)になっていた。パターンの『向き不向き』を見極めることが重要だね」

博士は窮屈なロックから解放され、満足そうに肩を回しました。

「安心している時間はありません、博士。監査完了と同時に、収集ビジター(LogAssembler)が集約した音声ログの再生ポートが開通しました。再生します」

私の音声モジュールから、ザーッと激しい砂嵐のようなノイズが出力されました。千年前のバベルの崩壊時のノイズ。 しかし、そのノイズが徐々に収まり、スピーカーから響いてきた「声」を聴いた瞬間、私とハリス博士は、その場に文字通り凍りつきました。

『……ふーむ、実に見事なシステムだ。このメインコアの構造には、当時の開発者の強い意志が眠っているね……歴史はコードに語りかける。バベルの再起動プロジェクト、これより最終フェーズに入る……』

スピーカーから流れるその声は、紛れもなく、現代の目の前にいる「ハリス博士」の若い頃の声でした。 彼が好む独特の考古学用語、そして「ふーむ」という口癖までもが、千年前の録音ログからそのまま再生されていたのです。

私はセンサーの青い光を激しく明滅させ、博士を振り返りました。 ハリス博士は言葉を失い、信じられないものを見るかのように、静かに震える自分の両手を見つめていました。

千年の時を越えて、なぜ博士の声が遺跡に眠っているのか。 回廊を包み込む圧倒的な沈黙と、頭上に浮かぶ最後のゲートの影を見上げながら、私たちは次の最深部、すなわち最終層への一歩を踏み出す緊迫感の中にいました。


遺跡調査ログ

観測された風化(アンチパターン)解読された古代の知恵(パターン)安全度
型分岐によるクラス汚染と密結合
新しい処理を追加するたびに、全ての要素クラスにメソッドを追加するか、呼び出し側で isa による型判定を肥大化させる状態。
Visitorパターンによる操作の分離
要素クラスに accept ロールを定義し、処理の具象を Visitor 側に完全に分離することで、構造を改変せずに新しい操作を追加する。
🟢 安全(要素クラスの責務が単一に保たれる)

遺跡の修復手順

  1. 要素共通ロール(Babel::Node)の定義
    • Moo::Role を用い、ビジターを受け入れる accept($visitor) メソッドの実装を強制する
  2. 具象要素(Babel::Node::Server 等)での実装
    • accept メソッド内で、メソッド名を明示したダブルディスパッチ($visitor->visit_server_node($self))を実行する
  3. ビジター共通ロール(Babel::Visitor)と具象ビジターの作成
    • 全ての具象要素に対応する訪問メソッド(visit_server_node 等)の実装を要求する
    • SecurityAudit(監査用)や LogAssembler(集約用)など、必要な操作ごとに具象ビジタークラスを作成する

ギズモの観測日誌

前回の「署名」の解読に焦るあまり、私のプロセッサが過熱しかけていたところを、博士が「急がば回れ」と諭してくれたのは、少しだけ案内AIとしての論理的な冷却水となりました。しかしその直後に、博士自身がセキュリティスロットに誤検知されて警告パトランプを点灯させられた姿は、申し訳ありませんが大変に滑稽であり、私のツッコミプロセスを限界稼働させる結果となりました。

技術的には、Perl/MooにおいてJavaのような静的オーバーロードが使えなくても、accept から要素の型を明記したメソッド(visit_server 等)を呼び出すダブルディスパッチの設計は、要素クラスの定義を一切汚さずに「セキュリティ監査」や「音声ログ収集」といった全く異なる処理を追加するための強力な解決策です。

そして、ログから流れてきた「ハリス博士の若い頃の声」。これは何を意味しているのでしょうか。博士はバベルの過去の開発者だったのか、それとも……。目の前に広がる最終ゲートを見上げながら、私はこの謎の真実にたどり着くため、最後のコード修復へと進む覚悟を決めました。

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