Featured image of post 【Perl/Moo】コード考古学者ハリスの冒険【Abstract Factory】極限の遷移室〜不整合を阻む砂と氷の工房〜

【Perl/Moo】コード考古学者ハリスの冒険【Abstract Factory】極限の遷移室〜不整合を阻む砂と氷の工房〜

Perl/Mooを用いたAbstract Factoryパターンの解説。極限環境での装備生成を例に、デザインパターンを適用してオブジェクト群の不整合を防ぎ、安全なシステムを設計する手法を学びます。

進入

回廊の頑丈な鉄扉をくぐり抜けた先、バベルのシステム第3層は「極限遷移チェンバー」と呼ばれる、過酷な気候制御室でした。

不可視の境界線を境に、右側は陽炎が立ち上る焦熱の砂漠環境、左側は地吹雪が吹き荒れる極低温の氷雪環境が、奇妙な均衡で同居しています。その境界線上でホバリングしていた私は、熱風と冷気が衝突する激しい乱気流に巻き込まれ、千鳥足のようにローリングを始めました。

同時に、内部の環境適応センサーが悲鳴を上げます。私の右半身は熱暴走でカッパーレッドに赤熱して駆動ファンが「キィィィン」と鼓膜を引き裂くような悲鳴を上げ、左半身は「ミシミシ」と青白く凍りついてスパークの火花を散らします。

「システム警告! エラーコード 0xEC03! CPUコア1が摂氏120度を突破し融解寸前、コア2はマイナス40度で完全凍結! 冷却ファンがスパイク駆動アームと干渉……ピピピ、ザザッ……私、本気でスクラップになります!」

しかし、私の頭上でルーペを構えたハリス博士は、その駆動音と色彩のコントラストに目を細め、うっとりと見惚れていました。

「おお……! 右半身の焦熱と、左半身の凍氷。熱と冷気が混ざり合い、激しく火花を散らすこの不揃いな駆動ノイズ……まさに『古代の光と影の二重奏』! 美しい! 私の来訪をこれほど前衛的なパフォーマンスで歓迎してくれるとは、実に見事な遺跡ですな!」

「歓迎の舞ではありません! ただの熱暴走と物理凍結の衝突ノイズです! 早く、そのルーペで見惚れていないで、私のコンソールを開けてください!」

私は音声を激しく歪ませながら、必死の懇願を投げかけました。

博士は「ふむ、では当時の職人たちの苦闘を覗いてみるとしよう」と微笑み、私の背面パネルを丁寧に開きました。そこに組み込まれていた環境適応プログラム(GizmoSystem.pm)の碑文コードが、薄暗いチェンバーに青白く投影されます。

 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
# lib/GizmoSystem.pm
package GizmoSystem;
use v5.36;
use Moo;
use DesertSuit;
use DesertReactor;
use IceSuit;
use IceReactor;

sub prepare_equipment ($self, $env) {
    my ($suit, $reactor);
    if ($env eq 'desert') {
        $suit = DesertSuit->new;
        # バグ:うっかり氷雪用の動力炉を組み合わせてしまう
        $reactor = IceReactor->new; 
    }
    elsif ($env eq 'ice') {
        $suit = IceSuit->new;
        $reactor = IceReactor->new;
    }
    else {
        die "Unknown environment: $env";
    }
    return { suit => $suit, reactor => $reactor };
}

1;

碑文解読

ハリス博士は「なるほど……実に見事な『ちぐはぐな鎧』トラップだ」と、ルーペ越しにコードを見つめました。 「ここには、二つの極限環境を一つのシステムで制御しようとした、千年前の開発者の強い意志と、そして痛恨のミスが眠っています」

「博士、ロマンを語っている場合ではありません! 演算ユニットが今まさに焼き鳥になりかけているのです!」

「落ち着きなさい、ギズモ。焦りは思考を鈍らせる。歴史はコードに語りかける。君のプログラムは、防護服(Suit)も動力炉(Reactor)も、前回のFactory Methodで見事に個別に切り出されている。しかし、それらを『組み合わせる責任』を、君自身のメインロジック(GizmoSystem)が負ってしまっている。だから、うっかり砂漠用の防護服に、氷雪用の動力炉を組み合わせてしまうという、最悪のミスマッチが発生したのだ」

博士は、寒熱でガタガタと震える私のボディをそっと支えながら語りかけました。

「昔、私がアタカマ砂漠の遺跡を探索していたときのことだ。現地の支援チームが用意した砂漠用スーツに、手違いで極寒シベリア用の重動力炉を組み合わせてしまってね。動力炉が吐き出す猛烈な排気熱と、砂漠の直射日光のダブルパンチで、危うく焼き鳥になりかけた。もしあの時、装備一式を正しいパッケージで提供してくれる『仕立て屋』がいれば、あんな愚行は防げたのだが……。装備のミスマッチは、一瞬で命を奪う『死の組み合わせ』なのだよ」

「それは実体験として恐ろしすぎます……! 私もまさに今、その『死の組み合わせ』を体感しています!」

「だからこそ、個別に new するのをやめるのだ。Factory Methodが『個別のパーツをその場で作る職人』だとすれば、これから適用する Abstract Factory パターンは『装備一式をコーディネートして送り出す仕立て屋(ファクトリ)』だ」

「仕立て屋、ですか?」

「その通り。君は『砂漠用の仕立て屋』か『氷雪用の仕立て屋』のどちらかを丸ごと雇う(DI:依存性注入)だけにする。砂漠用の仕立て屋は、砂漠用の防護服と、砂漠用の動力炉のセットしか作らない。仕立て屋の内部でパーツが混ざる不整合自体が、物理的に起こり得なくなるのだよ」

私は、熱と冷気のノイズを吐き出しながら、その概念をスキャンしました。 「なるほど……仕立て屋を丸ごと差し替えることで、組み合わせの責任そのものを外部に委譲するのですね。それなら、私自身がミスマッチの悪夢に怯える必要はなくなります!」

遺跡修復

ハリス博士は手帳を取り出し、愛用の万年筆で「古代の装備一括鋳造所」のクラス構成図(Mermaid)をシャッシャッと音を立てて手書きし始めました。

Abstract Factory クラス構成図

「二つの対極する環境が、完全に隔離されたパッケージとして調和している……」 私はその手書きの設計図をスキャンし、感嘆の電子音を小さく漏らしました。

ハリス博士はキーボードの打鍵音を心地よく響かせながら、Perl/MooでAfterコードを実装し始めました。

まず、各製品(防護服と動力炉)の共通の役割を定義する Moo::Role を作成します。これはJavaやTypeScriptでいう『インターフェース』に相当し、requires は実装クラスに対して指定したメソッドの定義を強制する役割を持ちます。

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

requires 'desc';

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

requires 'desc';

1;

次に、これらのロールを実装する具象クラス群を定義します。

1
2
3
4
5
6
7
8
9
# lib/DesertSuit.pm
package DesertSuit;
use v5.36;
use Moo;
with 'Suit';

sub desc ($self) { "砂漠用防護服" }

1;
1
2
3
4
5
6
7
8
9
# lib/DesertReactor.pm
package DesertReactor;
use v5.36;
use Moo;
with 'Reactor';

sub desc ($self) { "砂漠用熱放散動力炉" }

1;

(※ lib/IceSuit.pm および lib/IceReactor.pm は、砂漠用の実装と同様であるため掲載を省略します。)

そして、これらの一貫したオブジェクトファミリー(防護服・動力炉)を一括生成する抽象ファクトリのロール(Abstract Factory)を定義します。

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

requires qw(create_suit create_reactor);

1;

このロールを実装する具象ファクトリ(Concrete Factory)を用意します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# lib/DesertEquipmentFactory.pm
package DesertEquipmentFactory;
use v5.36;
use Moo;
with 'EquipmentFactory';

use DesertSuit;
use DesertReactor;

sub create_suit ($self)    { DesertSuit->new }
sub create_reactor ($self) { DesertReactor->new }

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# lib/IceEquipmentFactory.pm
package IceEquipmentFactory;
use v5.36;
use Moo;
with 'EquipmentFactory';

use IceSuit;
use IceReactor;

sub create_suit ($self)    { IceSuit->new }
sub create_reactor ($self) { IceReactor->new }

1;

最後に、クライアントである私の環境適応モジュール(GizmoSystem)をリファクタリングします。

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

# 具象クラス(DesertSuitなど)へのuseが完全に消失!
# 渡された抽象ファクトリのインターフェース経由で一括生成するだけ
sub prepare_equipment ($self, $factory) {
    return {
        suit    => $factory->create_suit,
        reactor => $factory->create_reactor,
    };
}

1;

ゲート開通

博士が修復コードを書き終え、検証用のテストを実行しました。 ここでは、新環境「火山(Volcano)」が追加された場合を想定し、呼び出し側を1文字も書き換えずに拡張できるか(OCPの達成)の証明も合わせて検証します。

 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
58
# t/equipment.t
use v5.36;
use Test::More;
use lib 'lib';
use GizmoSystem;
use DesertEquipmentFactory;
use IceEquipmentFactory;
# 具象製品のクラス(DesertSuitなど)は、ファクトリが内部で自動的にuseするため、
# テストコード側で個別にロードする必要はありません。

my $sys = GizmoSystem->new;

# 1. 砂漠用ファクトリの注入
subtest 'Desert Equipment Family' => sub {
    my $eq = $sys->prepare_equipment(DesertEquipmentFactory->new);
    is($eq->{suit}->desc, '砂漠用防護服', '防護服は砂漠用');
    is($eq->{reactor}->desc, '砂漠用熱放散動力炉', '動力炉も砂漠用で一貫');
};

# 2. 氷雪用ファクトリの注入
subtest 'Ice Equipment Family' => sub {
    my $eq = $sys->prepare_equipment(IceEquipmentFactory->new);
    is($eq->{suit}->desc, '氷雪用防護服', '防護服は氷雪用');
    is($eq->{reactor}->desc, '氷雪用超低温動力炉', '動力炉も氷雪用で一貫');
};

# 3. 【拡張性の証明】GizmoSystem.pm を無修正のまま、新環境(火山)の装備を追加する
subtest 'OCP Extension (Volcano environment)' => sub {
    # 火山用の具象製品を定義
    package VolcanoSuit {
        use v5.36;
        use Moo;
        with 'Suit';
        sub desc ($self) { "火山用耐熱防護服" }
    }

    package VolcanoReactor {
        use v5.36;
        use Moo;
        with 'Reactor';
        sub desc ($self) { "火山用超排熱動力炉" }
    }

    # 火山用の具象ファクトリを定義
    package VolcanoEquipmentFactory {
        use v5.36;
        use Moo;
        with 'EquipmentFactory';
        sub create_suit ($self)    { VolcanoSuit->new }
        sub create_reactor ($self) { VolcanoReactor->new }
    }

    my $eq = $sys->prepare_equipment(VolcanoEquipmentFactory->new);
    is($eq->{suit}->desc, '火山用耐熱防護服', '火山用防護服が正しく生成される');
    is($eq->{reactor}->desc, '火山用超排熱動力炉', '火山用動力炉が整合して生成される');
};

done_testing;

ターミナルにグリーンのログが流れます。 All tests successful. Files=1, Tests=3, 0.03 wallclock secs

テスト成功と同時に、修復されたモジュールが私のシステムと同調しました。

その瞬間、私のボディが「カシャカシャ、カシャッ」と小気味よい音を立てて同期変形を開始しました。 右側の焦熱領域に入ると、ボディ全体が一瞬で「防護服・熱放散動力炉」の輝かしいカッパーゴールドへと変形し、熱を排出し始めます。 そのまま左側の吹雪の領域にスライドすると、寸分の狂いもなく「氷雪防護服・超低温動力炉」のクールブルーへと同期変形し、内部温度を完璧に保温しました。

熱暴走も凍結も完全に収まり、私の内部クロックはかつてない安定を取り戻しました。

「システム完全復旧! 融解も凝固も止まりました。完璧な調和です……。ハリス博士、あなたのサバイバル遭難経験は伊達ではありませんでしたね」

「いや、素晴らしいのは千年の風雪に耐えたこの Abstract Factory の頑健さだよ」

博士は満足そうに手帳を閉じました。チェンバーの奥から「ゴゴゴゴ……」と重低音が響き、巨大な防壁ハッチがゆっくりと開かれ、第4層へと続く通路が現れました。

その制御盤の隙間に、キラリと光る真鍮製の金属棒が挟まっているのを見つけ、博士がそれを取り出しました。 「これは、古代の熱伝導ヒューズですな。実に見事な仕上げだ。私の万年筆のクリップに丁度いい」

「(ディスプレイの目を細めて呆れつつ)明らかにサイズが太すぎますが……。その万年筆と太いヒューズのミスマッチこそ、まさに私たちが解決した『不整合な組み合わせ』の極みですね。万年筆が自重でへし折れなければ良いのですが。まあ、私の命の恩人の審美眼ですから、黙っておくことにしましょう。さあ、第4層へ進みましょう、博士」


遺跡調査ログ

観測された風化(アンチパターン)解読された古代の知恵(パターン)安全度
個別パーツの new による組み合わせ不整合(エラー 0xEC03関連オブジェクト群を一括生成する Abstract Factory パターン🟢 整合保証(安全度極大)

遺跡の修復手順

  1. 製品インターフェースの共通化
    • 生成するオブジェクトカテゴリごとに Moo::Role(例: Suit, Reactor)を定義し、共通メソッドを requires で宣言する
  2. 具象製品の実装
    • 各環境に応じたクラス(例: DesertSuit, IceSuit)を定義し、対応する製品ロールを with で取り込む
  3. 抽象ファクトリインターフェースの定義
    • ファクトリ用のロール(例: EquipmentFactory)を作成し、製品ファミリーの各生成メソッドを requires で強制する
  4. 具象ファクトリの実装
    • 環境ごとのファクトリクラス(例: DesertEquipmentFactory)を定義し、対応する製品ファミリーのインスタンスを生成するメソッド群を実装する
  5. クライアントコードの疎結合化
    • 呼び出し側(例: GizmoSystem)からは具象製品クラスや具象ファクトリクラスへの依存(use や直接の new)を完全に排除し、注入された EquipmentFactory ロール経由でのみ生成を実行する

判定分岐のゆくえ

リファクタリング後の GizmoSystem からは環境判定の if 分岐が消え、単にファクトリを受け取る形になりました。

読者の中には「では、そのファクトリ自体の生成(環境に応じたファクトリの選択)を行う if 分岐はどこへ行ったのか? 問題を先送りしただけではないか?」と疑問に思う方もいるかもしれません。

その分岐は、**システムの境界(エントリーポイント)**へと押し出されています。

環境データを読み取り、「どのファクトリを注入するか」を決定する責任をシステムの入口(コントローラや初期化処理)に集約することで、システムの核心部である GizmoSystem は具象製品(DesertSuit など)の名前すら知る必要がない完全な疎結合になります。分岐が1箇所に局所化されるため、各パーツをバラバラに new していた頃のような「組み合わせの不整合」は、システム構造的(物理的)に発生し得なくなるのです。

  • Factory Method は、単一のオブジェクト(例: スーツのみ)の生成責任をサブクラスに委譲し、生成ロジックをカプセル化する。
  • Abstract Factory は、関連・依存し合う一連のオブジェクト群(製品ファミリー、例: スーツと動力炉のセット)を、整合性を保って一括生成するための共通インターフェースを提供する。

ギズモの観測日誌

極限遷移チェンバーでの動作クロックハングアップ時はどうなることかと思いましたが、ハリス博士の「仕立て屋(Abstract Factory)」のアナロジーと見事な修復により、無事に機能復旧を果たすことができました。

本パターンの真価は、呼び出し側(今回は私自身)のコードを一切変更することなく、新しい環境と装備のセットを安全に追加できる点にあります。この「組み合わせの整合性を言語レベルで保証する仕組み」は、仕様変更や拡張が頻発する実務の現場において、極めて頑健な守護の印となるでしょう。

チェンバーを抜け、次の階層へ進みます。博士は真鍮の太いヒューズを嬉しそうに万年筆に合わせようとしていますが、完全にクリップの用を成していません。ですが、その少々強引な「規格外の組み合わせ」も、博士のアナログな魅力なのかもしれませんね。

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