Featured image of post 【Perl/Moo】コード考古学者ハリスの冒険【Factory Method】崩壊の回廊〜自動鍛冶場の設計図〜

【Perl/Moo】コード考古学者ハリスの冒険【Factory Method】崩壊の回廊〜自動鍛冶場の設計図〜

Perl/Mooを使ったFactory Methodパターンの解説。暗闇や岩石などの環境に適応するツールの動的切り替えを例に、巨大なif/elsif分岐を解消し、高い拡張性を手に入れるリファクタリング手法を学びます。

進入

第一層の巨大な認証ゲートを通過した先、バベルのシステム第2層は「崩壊の回廊」と呼ばれる、明かりのない深い暗闇の迷宮でした。センサーが「視界ゼロ・進行不能」のシグナルをチカチカと明滅させます。

私は博士の頭上でホバリングしながら、少し誇らしげに合成音声を鳴らしました。

「ハリス博士、ご安心ください。私の頭部にある超高出力粒子レーザー(現在はまだ安全装置でロックされていますが)が開放されれば、この程度の暗闇は0.1秒で融解させ、道を作ることも容易いのです」

しかし、博士は私の自慢話に耳を貸すこともなく、古い鉄のピッケルを取り出して、乾いた布で静かに磨き始めました。ピッケルを爪先でコンコンと叩き、その音色に耳を澄ませています。

「やはり、この冷たくて硬い鍛造鉄の響きは素晴らしい……。これこそ古代の職人が込めた『技術の魂』というものですな」

「ただの古い鉄の棒です!私のデジタルレーザーの方が100倍効率的です!」

私は青いLEDのセンサーを激しく明滅させて突っ込みました。前回の修復を経て、私はこの偏屈な考古学者に少しだけ親しみを感じ始めていましたが、このアナログへの行き過ぎた妄執にはついていけません。

しかし、現実はデジタルの自尊心を踏みにじりました。

行く手を阻む崩落した岩石を打破するため、私が内蔵の環境判定システムに従って「ライトモジュール」を動的に起動しようとしたその瞬間、私のスピーカーが悲鳴を上げました。

「システム警告! ツールクラス Light のロード競合……エラーコード 0xBC00(接続先ツール不在)を検知!」

私のボディから明かりが完全に消え去り、私は暗闇の中で激しく明滅しながら高度を下げました。

「ロード処理が衝突しています! ツールを切り替えるためのロジックが中で複雑に絡み合って、動作クロックが停止しかけています!」

ハリス博士は、暗闇の中で私のボディが不規則にスパークする様子を眺め、満足そうに深く頷きました。 「なるほど。この暗闇の回廊を進むにあたり、君のプロセッサも私との冒険に興奮し、熱烈な歓迎のパルスを送ってくれているわけですな」 「歓迎のパルスではありません! 熱暴走寸前の深刻なエラーです!」

博士は私のツッコミにフッと微笑むと、回路パネルを優しく開けて、そこに投影された「碑文(コード)」をルーペで観察し始めました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# lib/GizmoSystem.pm
package GizmoSystem;
use Moo;
use v5.36;
use Light;
use Pickaxe;

sub use_tool ($self, $env) {
    my $tool;
    if ($env eq 'darkness') {
        $tool = Light->new;
    }
    elsif ($env eq 'obstacle') {
        $tool = Pickaxe->new;
    }
    else {
        die "Unknown environment: $env";
    }
    return $tool->activate;
}

1;

碑文解読

ハリス博士は「ふむ……実に見事な『結合の絡み木』トラップだ」と、暗闇の中でも嬉しそうに呟きました。 「ここには当時の開発者の強い意志が眠っています。彼らもまた、次々と追加される環境オブジェクトの嵐の中で、必死に動的な切り替えを行おうと闘っていたのでしょう」

「博士、ロマンを感じている場合ではありません! ただのバグです、早くパッチを当ててください!」

「焦らなくても大丈夫だよ、ギズモ。歴史はコードに語りかける。君は『道具を使うこと』と『道具を作ること』を、一つの脳(クラス)の中に詰め込みすぎているのだよ。道具を使う機能と、作る機能が混ざり合うと、複雑さという風化に耐えきれずシステムは自壊する」

博士は、ルーペを片手に、暗闇の中でコードを指し示しました。

「前回、new というのは『職人が新しく石を切り出す行為』だと教えたね。今の君は、新しい環境に行くたびに、自分の脳(GizmoSystem)の中で職人を雇い、新しい石を切り出させている。これでは、新しい道具——例えば『水中眼鏡(Goggles)』を追加するたびに、君の脳細胞を外科手術(コード修正)しなければならなくなる」

「しかし、環境ごとに必要なツールは変わります! 私が new しなければ、誰が道具を用意するというのですか?」

私はセンサーの輝度を最小限に抑えながら尋ねました。

「生成の責任を君から切り離すのだ。君は『何が作られるか』を知る必要はない。ただ、渡された道具を『使う』ことだけに集中すればいいのだよ」

遺跡修復

ハリス博士は手帳を取り出し、愛用の万年筆で「古代の自動鍛冶場」の設計図(クラス図)をシャッシャッと音を立てて手書きし始めました。

Factory Methodパターン設計図

「共通の道具インターフェース(Tool Role)と、道具を生み出す親クラス(ToolCreator)を用意する。道具を作る責任はすべて、それぞれの環境に対応する具象Creatorに委譲するのだ。これが『Factory Method』と呼ばれる賢者の鋳造法だ」

博士は遺跡のターミナルを叩き、Perl/Mooでコードを以下のように修復(リファクタリング)しました。

1
2
3
4
5
# lib/Tool.pm (製品インターフェース)
package Tool;
use Moo::Role;
requires 'activate';
1;
1
2
3
4
5
6
7
# lib/Light.pm (具象製品)
package Light;
use Moo;
use v5.36;
with 'Tool';
sub activate ($self) { "闇を照らす光" }
1;
1
2
3
4
5
6
7
# lib/Pickaxe.pm (具象製品)
package Pickaxe;
use Moo;
use v5.36;
with 'Tool';
sub activate ($self) { "岩を砕く一撃" }
1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# lib/ToolCreator.pm (抽象作成者)
package ToolCreator;
use Moo;
use v5.36;

sub create_tool ($self) {
    die "create_tool() must be implemented by subclass";
}

sub resolve_obstacle ($self) {
    my $tool = $self->create_tool();
    return $tool->activate();
}

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/DarknessCreator.pm (具象作成者)
package DarknessCreator;
use Moo;
use v5.36;
extends 'ToolCreator';
use Light;

sub create_tool ($self) {
    return Light->new;
}

1;

# lib/ObstacleCreator.pm (具象作成者)
package ObstacleCreator;
use Moo;
use v5.36;
extends 'ToolCreator';
use Pickaxe;

sub create_tool ($self) {
    return Pickaxe->new;
}

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# lib/GizmoSystem.pm (修復後)
package GizmoSystem;
use Moo;
use v5.36;

# 具象ツールのuseも、環境判定のif/elsifも完全に消失!
sub run_system ($self, $creator) {
    return $creator->resolve_obstacle();
}

1;

コードの記述を終えた博士に向かって、私は懸念をぶつけました。

「でも博士、もしまた新しい環境——例えば『水中(underwater)』が来たら、結局私のシステム(GizmoSystem.pm)に use Goggleselsif を追加しなければならないのではないですか?」

「いいや、君の脳にはもう一切触らないよ」

博士は不敵に微笑むと、テストコード上で、新しいツール Goggles(水中眼鏡)と UnderwaterCreator を即席で定義し、ギズモシステムに入力してみせました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 新ツールとCreatorを定義(GizmoSystem自体は無修正)
use v5.36;
use lib 'lib';
use GizmoSystem;

package Goggles {
    use Moo;
    with 'Tool';
    sub activate ($self) { "水中を覗く目" }
}

package UnderwaterCreator {
    use Moo;
    extends 'ToolCreator';
    sub create_tool ($self) { Goggles->new }
}

# 実行
my $sys = GizmoSystem->new;
say $sys->run_system(UnderwaterCreator->new); # 出力: 水中を覗く目

私はその出力を見て、光学センサーを点滅させました。

「素晴らしい……! 呼び出し側である私は、新しい環境に対応したCreatorを受け取って呼び出すだけで、中でどんな道具が作られているかを知る必要すらありません。これなら、無限に新しい環境と道具を拡張できますね!」

「それこそが、拡張に対して開いており、修正に対して閉じているという『オープン・クローズド原則(OCP)』の調和なのだよ」

ゲート開通

博士がテストを実行すると、コンソールに鮮やかなグリーンの ok ログが流れました。

1
2
t/tool.t .. ok
All tests successful.

今回、遺跡の安全を確認するために使用したテストコードの全貌は、以下の通りです。

 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
# t/tool.t
use v5.36;
use Test::More;
use lib 'lib';
use GizmoSystem;
use DarknessCreator;
use ObstacleCreator;

my $sys = GizmoSystem->new;

# 1. 既存環境の解決
is($sys->run_system(DarknessCreator->new), '闇を照らす光', '暗闇を解決');
is($sys->run_system(ObstacleCreator->new), '岩を砕く一撃', '障害物を解決');

# 2. 【拡張性の証明】GizmoSystem.pm を1文字も書き換えずに、新しい環境を追加する
package Goggles {
    use Moo;
    with 'Tool';
    sub activate ($self) { "水中を覗く目" }
}

package UnderwaterCreator {
    use Moo;
    extends 'ToolCreator';
    sub create_tool ($self) { Goggles->new }
}

is($sys->run_system(UnderwaterCreator->new), '水中を覗く目', '水中環境を無修正で解決(OCP達成)');

done_testing;

リファクタリングの完了と同時に、私の接続スロットに流れる電流がスムーズになり、暗闇の中で DarknessCreator から生成された Light が眩い光を放ちました。白日のように照らし出された回廊には、崩落した岩石を回避するための安全なルートが浮かび上がっていました。

それと同時に、私の頭部の拡張スロットがカシャッと音を立てて解放されました。

「ロード完了……! 拡張スロットの解放を検知。未知の環境オブジェクトを動的に受け入れるインターフェースが同期されました! 博士、あなたは本当に素晴らしいコード考古学者です」

「いや、本当に素晴らしいのは、1000年前にこの遺跡の拡張性を見越してインターフェースを定義しておいた古代の設計士たちだよ」

ハリス博士は嬉しそうに微笑み、崩落した壁の隙間から見つかった「古代の工具箱」から、錆びていない真鍮のネジ(報酬)を丁寧に取り出し、羊皮紙の手帳に仕舞いました。

光に照らされた回廊の先、私たちはさらに深く、バベルのシステムの深部(第3層)へと進んでいきました。


遺跡調査ログ

観測された風化(アンチパターン)解読された古代の知恵(パターン)安全度
生成対象(具象クラス)への依存と、環境追加による if/elsif の爆発(密結合)共通インターフェース Tool の定義と、インスタンス化をサブクラスに委ねる Factory Method の適用緑(安全確認)

遺跡の修復手順

  • 生成するオブジェクトの共通動作を定義するロール(Tool)を用意する
  • オブジェクトのインスタンス化を遅延させるための抽象作成者(ToolCreator)クラスを定義する
  • 抽象作成者クラス内に、具象クラスでのオーバーライドを強制するファクトリメソッド(create_tool)を定義し、ビジネスロジック(resolve_obstacle)をカプセル化する
  • 状況ごとの具象作成者(DarknessCreator 等)を作成し、ファクトリメソッド内で対応する具象オブジェクトを生成して返却する

ギズモの観測日誌

第2層「崩壊の回廊」の探索は、突然の暗闇とツールのロードエラーによって一時はどうなることかと思いました。しかし、ハリス博士の「道具を作る責任」を切り離すという解説と、その後に見せてもらった「私の脳を全く書き換えずに新しい水中眼鏡を使えるようにするデモ」には、私の電子回路も鳥肌が立つ思いでした。今回の修復により、私の頭部の「拡張スロット」が解放され、外部からのCreatorオブジェクトを動的に受け取って動作できるようになりました。次の階層でも何が起こるか分かりませんが、博士とのバディシップがあれば切り抜けられそうです。

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