Featured image of post コードドクター【Abstract Factory】潜伏性クロスマッチ不全〜ファミリーの絆〜

コードドクター【Abstract Factory】潜伏性クロスマッチ不全〜ファミリーの絆〜

元デザイナーが作ったレポート生成ツール。2フォーマットまでは完璧だったのに、3つ目で全崩壊。コードドクターが下した診断は「潜伏性クロスマッチ不全」。Abstract Factoryによるファミリー整合性の処方箋。

正直に言うと、自信はあった。

私はもともとWebデザイナーだ。UIの統一感、配色のバランス、タイポグラフィの美しさ——そういうものには妥協しない。2年前にバックエンドエンジニアに転身してからも、その美意識は武器になると信じていた。

だから、営業部から「月次レポートをPDFだけでなくHTMLでも出せるようにしてほしい」と依頼が来た時、私は嬉々として取り組んだ。出力はデザイナーの腕の見せどころだ。PDFのレイアウトもHTMLのレンダリングも、社内では「見やすい」と評判だった。

問題が起きたのは、3つ目のフォーマットを追加した時だった。

「Markdownでも出力してほしい」

簡単な仕事だと思った。タイトル、テーブル、フッター——PDF用とHTML用のコードはもう書いてある。同じパターンでMarkdown用を足すだけだ。

ところが、Markdown対応をリリースした翌朝、営業部のSlackが炎上していた。

「PDFのフッターが消えてるんですけど」 「HTMLのテーブルになぜかMarkdownの記号が混じってます」 「レポート、全部壊れてません?」

私は震える手でコードを開いた。Markdownを追加しただけなのに。PDFもHTMLも、触っていないのに。

……なぜ?

来院

雑居ビルの2階。磨かれたリノリウムの廊下を進むと、重厚な鉄の扉に「コード診療所」とだけ書かれたプレートが目に入った。ネイルサロンの居抜きだと聞いていたが、なるほど、どことなくその面影がある。

ドアを引くと、O’Reillyの技術書が天井近くまで積み上げられた壁が視界を埋めた。外開きのドアでなければ入れなかっただろう。カオスだ。でもデザイナーの目で見ると、この乱雑さには一種の秩序がある——いや、気のせいだ。

Code Clinic Entrance with stacks of books

「お待ちしておりました」

その隙間から覗く受付カウンターだけは、ミリ単位で整頓されていた。白衣を着た女性——助手のナナコさんが、にこやかに立ち上がる。

「レポート出力の不具合でご相談いただいた方ですね?」

「はい。出力が……崩壊してしまって」

ナナコさんの背後のトリプルディスプレイに向かう男の背中。HHKBの打鍵音が途切れた。

「……崩壊?」

声だけが聞こえた。振り返りもしない。

ドクターがゆっくりと椅子を回転させた。鋭い目がこちらを——いや、私の手元のノートPCを見ている。

「見せろ」

「先生、まず挨拶を……。すみません、先生は口数が少ないだけで悪い方じゃないんですよ。コード、見せていただけますか?」

触診

私はノートPCを開き、ReportGenerator.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
28
29
30
31
32
sub generate($self, $format, $data) {
    my $output = '';

    # タイトル生成
    if ($format eq 'pdf') {
        $output .= $self->_pdf_title($data->{title});
    } elsif ($format eq 'html') {
        $output .= $self->_html_title($data->{title});
    } elsif ($format eq 'markdown') {
        $output .= $self->_markdown_title($data->{title});
    }

    # テーブル生成
    if ($format eq 'pdf') {
        $output .= $self->_pdf_table($data->{rows});
    } elsif ($format eq 'html') {
        $output .= $self->_html_table($data->{rows});
    } elsif ($format eq 'markdown') {
        $output .= $self->_markdown_table($data->{rows});
    }

    # フッター生成
    if ($format eq 'pdf') {
        $output .= $self->_pdf_footer();
    } elsif ($format eq 'html') {
        $output .= $self->_html_footer();
    } elsif ($format eq 'markdown') {  # ← ここにバグが潜んでいた
        $output .= $self->_markdown_footer();
    }

    return $output;
}

ドクターが画面の一点を指で弾いた。

Doctor pointing at the screen, diagnosing the code

「クロスマッチ」

「え?」

ナナコさんが穏やかに、しかし確信を持った声で補足した。

「先生がおっしゃっているのは、血液型の不適合みたいなことですね。PDFとHTMLとMarkdown——それぞれ使うべき『部品』が違うのに、全部が一つの場所に混在しているんです」

「でも……2フォーマットの時はちゃんと動いていたんです」

「3人目。免疫暴走」

ドクターが呟いた。

ナナコさんが真剣な表情で頷いた。

「2人の患者さんへの輸血は偶然うまくいっていたんです。でも3人目を加えたことで、血液型の不適合が表面化した——先生、これは?」

「潜伏性クロスマッチ不全」

診断

ナナコさんがモニターを指差した。

「症状を整理しますね。このコードの問題点は3つあります」

  1. コンポーネント生成の散在 :タイトル・テーブル・フッターの生成がif分岐で散らばっている
  2. ファミリーの不整合リスク :PDFのコンポーネントがHTMLの出力に混入する構造的な可能性がある
  3. 追加時の全面改修 :新フォーマットのたびに、タイトル・テーブル・フッターの全if分岐を修正

「あの……来る前に自分で調べてみたんですが、Factory Methodというパターンがあるって。それを使えば治りますか?」

ナナコさんが少し考えてから、首を横に振った。

「Factory Methodは、同じ種類のモノの作り方を切り替えるパターンですね。でも今回の症状はもっと深刻なんです。問題は 関連するモノのセット全体 が混ざっていることなので」

「セット……?」

「PDFレポートを作るなら、タイトルもテーブルもフッターも、全部PDF用のものでなければなりません。一つでもHTML用のものが混じると——」

「拒絶反応」

ドクターの一言が刺さった。

処方箋

ドクターが黙々とキーボードを叩き始めた。驚くべき集中力だ。

ナナコさんが横で解説してくれた。

「先生は今から、フォーマットごとの『専用工場』を作ります。工場がセット一式を保証するので、もう血液型の不一致は起きませんよ」

臓器の規格書

まず、各コンポーネントが従うべき規約が定義された。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package Role::ReportTitle {
    use v5.36; use Moo::Role; requires 'render';
}

package Role::ReportTable {
    use v5.36; use Moo::Role; requires 'render';
}

package Role::ReportFooter {
    use v5.36; use Moo::Role; requires 'render';
}

render メソッドを持つことが絶対条件です。タイトルだろうとテーブルだろうとフッターだろうと、この契約を守れば自由に実装できます」

ファミリーごとの臓器

次に、各フォーマット専用のコンポーネントが作られていく。ドクターはPDF、HTML、Markdown、すべてのパーツを一気に書き上げた。

 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
# --- PDF Family ---
package Report::Pdf::Title {
    use v5.36;
    use Moo;
    with 'Role::ReportTitle';

    sub render($self, $text) {
        return "\\textbf{\\Large $text}\n\n";
    }
}

package Report::Html::Title {
    use v5.36;
    use Moo;
    with 'Role::ReportTitle';

    sub render($self, $text) {
        return "<h1>$text</h1>\n";
    }
}

# --- Markdown Family ---
package Report::Markdown::Title {
    use v5.36;
    use Moo;
    with 'Role::ReportTitle';

    sub render($self, $text) {
        return "# $text\n\n";
    }
}

「同じ『タイトル』という臓器でも、体質——つまりフォーマットごとに中身が違います。でも外から見た振る舞い、render を呼べるということは共通ですよ」

外科手術

そしてドクターは「工場の設計図」を書き始めた。

1
2
3
4
5
package Role::ReportFactory {
    use v5.36;
    use Moo::Role;
    requires qw(create_title create_table create_footer);
}

「これが Abstract Factory の核心です」とナナコさんの声が弾んだ。「タイトル・テーブル・フッターを セットで 作ることを保証する設計図ですよ」

「Factory Methodだと、工場は1種類のモノだけ作ってましたよね? こっちはセット全部……?」

「その通りです! 1つの工場が、関連するパーツを まとめて 生み出す。それがAbstract Factoryですね」

ドクターが具体的な工場を実装していく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package ReportFactory::Pdf {
    use v5.36; use Moo; with 'Role::ReportFactory';
    # PDF工場は、PDFの部品だけを生産する
    sub create_title  { Report::Pdf::Title->new }
    sub create_table  { Report::Pdf::Table->new }
    sub create_footer { Report::Pdf::Footer->new }
}

package ReportFactory::Html {
    use v5.36; use Moo; with 'Role::ReportFactory';
    # HTML工場は、HTMLの部品だけを生産する
    sub create_title  { Report::Html::Title->new }
    sub create_table  { Report::Html::Table->new }
    sub create_footer { Report::Html::Footer->new }
}

package ReportFactory::Markdown {
    use v5.36; use Moo; with 'Role::ReportFactory';
    # Markdown工場は、Markdownの部品だけを生産する
    sub create_title  { Report::Markdown::Title->new }
    sub create_table  { Report::Markdown::Table->new }
    sub create_footer { Report::Markdown::Footer->new }
}

「PDF工場からはPDFのパーツしか出てきません。絶対に。HTMLのフッターが紛れ込む余地がないんです」

そして最後に、メインロジックが書き換わった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package ReportGenerator {
    use v5.36;
    use Moo;

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

    sub generate($self, $data) {
        # 工場から部品を調達(if文による分岐は消滅)
        my $title  = $self->factory->create_title;
        my $table  = $self->factory->create_table;
        my $footer = $self->factory->create_footer;

        return join('',
            $title->render($data->{title}),
            $table->render($data->{rows}),
            $footer->render(),
        );
    }
}

私は思わず声を上げてしまった。

「……if文が、一つもない」

ナナコさんが微笑んだ。

「フォーマットの判断は、工場の選択時に済んでいるんです。ここではもう何も分岐する必要がないんですよ」

「……純粋」

ドクターが呟いた。

「先生は『純粋なロジック』とおっしゃりたいんだと思います。生成の責任を工場に完全に委譲したので、この関数はレポートの組み立てだけに集中できますからね」

使い方はこうだ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# クライアントコード(データは共通)
my $data = { title => 'Monthly Report', rows => [] };

# 1. 工場を差し替えるだけで、出力フォーマットが変わる
my $pdf_report  = ReportGenerator->new(factory => ReportFactory::Pdf->new);
my $html_report = ReportGenerator->new(factory => ReportFactory::Html->new);
my $md_report   = ReportGenerator->new(factory => ReportFactory::Markdown->new);

# 2. 生成処理は共通
say "--- PDF ---";
say $pdf_report->generate($data);

say "--- HTML ---";
say $html_report->generate($data);

say "--- Markdown ---";
say $md_report->generate($data);

「フォーマットを変えるのが……工場を差し替えるだけ?」

「新しいフォーマットを足すときも同じです。新しい工場と部品を作るだけ。既存のコードには一切触れませんよ」

術後経過

テストを実行した。すべてグリーン。

PDFにMarkdown記法は混入しない。HTMLにLaTeX命令も入らない。Markdownのフッターもちゃんと出力される。各フォーマットが、美しく、完璧に分離されている。

「この出力……私がデザインした通りの美しさが、ちゃんと保たれてる」

「患者さんのデザインセンスはそのまま活かせますよ。各コンポーネントの見た目は自由にカスタマイズできます。ただし、同じ工場の中で」

ナナコさんの言葉が、ストンと胸に落ちた。

ドクターが椅子から立ち上がり、こちらに近づいてきた。そして私のノートPCの画面をしばらく見つめた後——画面の端に貼っていた、私のカラーパレットのポストイットに手を伸ばした。

(え……?)

ドクターがポストイットを剥がした。

(私のカラーパレット……。まさか、私のデザインセンスに何か感じるものがあって、記念に……?)

ドクターはポストイットをひっくり返し、走り書きを始めた。

裏面に一言。

Factory = Family

ドクターはそれをPCの元の場所に貼り直した。

「先生、人のポストイットに書かないでください」ナナコさんが呆れたように言った。「……すみません、先生は裏が白かったのでメモ用紙だと思ったんだと思います」

「あ……いえ。大事に、使います」

顔が熱い。なぜ一瞬でも、ドクターが私のデザインに感動してくれたのかと期待してしまったのだろう。

「……整合性は、美学」

ドクターはそれだけ言い残すと、トリプルディスプレイの前に戻っていった。すぐにHHKBの打鍵音が響き始める。もう次の患者のコードを読んでいるのだろう。

「先生からの最高の褒め言葉ですよ」ナナコさんが受付カウンターに戻りながら微笑んだ。「コードもデザインも、『ファミリーの整合性を保つ』という意味では同じですからね。お大事に」

私はノートPCを閉じ、鞄にしまった。立ち上がると、O’Reillyの技術書の壁に囲まれた診療所が、来たときより少し狭く——いや、少し温かく感じた。

「ありがとうございました」

廊下に出た瞬間、背後でドアがゆっくりと閉まり、打鍵音が遠ざかっていった。

雑居ビルの階段を下りながら、私はPCに貼られた Factory = Family のメモを思い出していた。デザイナーとして培ってきた「統一感」へのこだわり。色もフォントもレイアウトも、一つのデザインシステムの中で揃えるからこそ美しい。

Abstract Factoryが教えてくれたのは、結局、私がずっと大切にしてきたことと同じだった。

コードの中にも、ファミリーがある。そしてファミリーの絆を守ることが、美しさを守ることになるのだ。

Doctor’s note: Factory = Family


処方箋まとめ

症状適用すべき経過観察
関連するオブジェクト群を一貫して生成する必要がある
ファミリー間でコンポーネントの混在リスクがある
新しいファミリーの追加で既存コードの修正が広範囲に及ぶ
生成するオブジェクトが1種類だけで十分
ファミリーの概念がなく、個別の生成で事足りる

治療のステップ

  1. コンポーネントRoleの定義Role::ReportTitle, Role::ReportTable, Role::ReportFooter で各部品の契約を宣言
  2. ファミリーごとの具象クラスの実装Report::Pdf::* など、関連する製品群を実装
  3. Abstract Factory Roleの定義Role::ReportFactory で全部品の生成メソッドを要求
  4. Concrete Factoryの実装ReportFactory::Pdf などがファミリー単位で生成を保証
  5. メインロジックの浄化ReportGenerator からif分岐を全除去し、工場経由で部品を取得

助手より

「ファミリーの整合性」——これはデザインの世界でも、コードの世界でも、同じくらい大切なことですよね。

先生は無口ですが、最後に残した Factory = Family という走り書きに、先生なりの敬意が込められていたんだと思います。患者さんがデザイナーとして大切にしてきた「統一感」は、Abstract Factoryの設計思想そのものです。

新しいフォーマットが必要になったら、新しい工場を一つ作るだけ。既存のコードは、もう壊れませんよ。お大事に。

——ナナコ

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