Featured image of post コードドクター【Bridge】二軸癒着症候群〜帳票地獄のピボット分離術〜

コードドクター【Bridge】二軸癒着症候群〜帳票地獄のピボット分離術〜

来院

月曜の朝は嫌いだ。

いや、正確に言えば、月曜の朝に部長から飛んでくるSlackの通知が嫌いだ。「ミサキさん、広報部のレポートも追加できる? あと、Markdown形式も欲しいんだけど」——そのたびに、私の胃がきゅっと縮む。

同僚の田中くんが「一度診てもらったほうがいい」と言って渡してくれたメモには、雑居ビルの住所と「コード診療所」という怪しげな名前だけが書かれていた。

私はカバンの中に、自分で作ったスプレッドシートの印刷物を忍ばせていた。A3用紙に印刷した、レポートシステムの全分岐パターン一覧表。行が部門名、列が出力形式。9つのセルのそれぞれに、対応するif文の行番号が几帳面に記入してある。Excel育ちの私の、せめてもの整理術だった。

雑居ビルの2階。磨かれたリノリウムの廊下を進むと、重厚な鉄の扉に突き当たった。プレートには飾り気のないゴシック体で「コード診療所」とだけ。看板もない。本当にここでいいのだろうか。

恐る恐る扉を引くと、まず目に飛び込んできたのはO’Reilly本の壁だった。ラクダ、リャマ、アルパカ——動物の表紙が天井近くまで積み上がっている。その隙間の向こうに、3枚のモニタの青白い光と、椅子に座った男の背中が見えた。振り向く気配はない。

「大丈夫ですよ、ここはコード診療所です」

カウンターの奥から、柔らかい声が聞こえた。白い上着を羽織った助手が、穏やかに微笑んでいる。

「あ……田中くんの紹介で来たんですが……」

「ナナコと申します。助手です。どうぞ、こちらへ」

ナナコさんに促されてカウンターの前まで来ると、私はカバンからスプレッドシートの印刷物を取り出した。

「あの、問題を整理してきました。私、もともと経理部でExcelをずっと触っていて……こうやって表にまとめるのが癖で」

広げたA3用紙を見て、ナナコさんの手が一瞬止まった。

9列×5行。部門と形式のマトリクス。各セルにはif文の行番号と、赤ペンで書き込まれた「ここが壊れた(2回目)」というメモ。

背後で、椅子が軋む音がした。

あの男——いや、先生らしき人物が、初めてこちらを向いた。立ち上がりもせず、椅子ごと滑るようにこちらへ来ると、私の表を一瞥した。

「……これは?」

低い声。ぶっきらぼうという表現がぴったりだった。

「コードの設計書です。Excelで整理しました」

先生は無言で紙を受け取ると、人差し指で行と列をなぞった。行を上から下へ。列を左から右へ。そして、視線を上げずに呟いた。

「……答えが、ここに書いてある」

私には、何のことだか分からなかった。

触診

「ノートPCを」

先生が片手を差し出した。渡すと、ものすごいスピードでコードを開き始めた。ファイルツリーを展開し、問題の関数——generate_report——を表示する。

200行を超えるif-elsifの群れが、モニタいっぱいに広がった。

先生は5秒ほど画面を走査し、眉をひそめた。そして、モニタの表面を人差し指でぴんと弾いた。

「……癒着(Adhesion)」

「え?」

ナナコさんがさっと私のそばに来て、噛み砕くように言った。

「部門のロジックと出力形式が完全に一体化しているということです。お腹の中で2つの臓器がべったりくっついてしまって、片方だけ治療しようにも、もう片方を傷つけてしまう——そんな状態ですね」

2つの臓器。私のコードが、そんなふうに見えるのか。

先生はさらにスクロールした。そして、止まった。

$cell_A1, $sheet_name, $col_header——

先生がゆっくりと振り向いた。

「……命名(Naming)。……Excel」

ナナコさんが困ったように微笑んだ。

「変数名がExcelの……セル番地になっていますね?」

顔が熱くなった。

「だ、だって考えやすいんです……A列の1行目だから $cell_A1 で……」

先生はもう聞いていなかった。私の印刷物に視線を戻し、行と列をそれぞれ人差し指でなぞっている。行(部門名)を縦に。列(形式名)を横に。

そして、マトリクスの中央で、2本の指を交差させた。

「二軸。……分離する」

先生が私のExcel表を使って何かを説明しようとしているのは分かった。でも、「分離」が何を意味するのか、このときの私にはまだ見えていなかった。

診断

ナナコさんが、私に向き直った。

「先生の診断をお伝えしますね。病名は 二軸癒着症候群(Biaxial Adhesion Syndrome) です」

「に、二軸……?」

「はい。あなたのコードには、2つの軸——部門出力形式——が存在しています。本来なら、この2つは独立して変化できるはずなんです。営業部のロジックが変わっても、CSV出力の仕組みには影響しない。逆もまた然り。ところが今の状態では、2つの軸が1つの巨大な関数の中で完全に癒着していて——」

「片方を触ると、もう片方も壊れる……」

思わず呟いた。まさに、私が毎週月曜に怯えていたことだ。

ナナコさんが頷いた。

「処方は Bridge ——癒着分離手術です」

外科手術

先生がおもむろに立ち上がり、私のスプレッドシートを壁に貼った。

「ちょ……私の資料——」

先生はポケットからペンを取り出すと、行と列を赤い線で囲んだ。行の束——営業、経理、人事。列の束——CSV、HTML、テキスト。

そして、引き出しからハサミを取り出した。

「え? えっ?」

ためらいもなく、先生はスプレッドシートを切った。行の短冊と列の短冊に。

「行。……Abstraction」

左の壁に、行の短冊を貼る。

「列。……Implementation」

右の壁に、列の短冊を貼る。

ナナコさんが私に向き直った。

「お持ちになった表を見てください。行が部門——営業、経理、人事。列が出力形式——CSV、HTML、テキスト。今まではこの9つのマスの中身を、全部1つの関数に詰め込んでいましたよね」

「は、はい……」

「先生は、行と列を 別々の部品 にしましょう、とおっしゃっているんです。部門のロジックと出力形式のロジックを切り離して、それぞれ独立したクラスにする。そして、使うときに組み合わせる」

頭の中で、何かがカチッと噛み合った。

「……あ。……これ、Excelのピボットテーブルと同じ考え方じゃないですか? 軸を分けて、あとから組み合わせる……」

ナナコさんが目を見開いた。

「……まさに、その通りです」

先生がほんの一瞬、私を見た。その視線に驚きの色が混ざったように——いや、気のせいかもしれない。すぐにモニタに向き直ってしまったから。

先生がキーボードに手を置いた。HHKB特有の、静電容量の打鍵音が心地よく響き始めた。

ナナコさんが小声で解説を始めた。

「まず、出力形式を ReportFormatter という部品にします。これが右の壁——Implementationです」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package ReportFormatter;
use v5.36;

sub new ($class, %args) {
    return bless \%args, $class;
}

# サブクラスでオーバーライドする
sub render_header ($self, $title) { die "not implemented" }
sub render_row    ($self, $label, $value) { die "not implemented" }
sub render_footer ($self) { die "not implemented" }

1;

「そして、CSV、HTML、テキストのそれぞれが、この部品を具体化します」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package ReportFormatter::CSV;
use v5.36;
use parent 'ReportFormatter';

sub render_header ($self, $title) { "" }

sub render_row ($self, $label, $value) {
    return "$label,$value\n";
}

sub render_footer ($self) { "" }

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package ReportFormatter::HTML;
use v5.36;
use parent 'ReportFormatter';

sub render_header ($self, $title) {
    return "<html><body><h1>${title}</h1>\n<table>\n";
}

sub render_row ($self, $label, $value) {
    return "<tr><th>$label</th><td>$value</td></tr>\n";
}

sub render_footer ($self) {
    return "</table>\n</body></html>\n";
}

1;

ナナコさんが左の壁を指した。

「次に、部門のロジックを DepartmentReport という部品にします。これが左の壁——Abstractionです。ここがポイントなのですが、DepartmentReportは出力形式の部品を 持っている んです」

「持っている?」

「is-a(〜である)ではなく、has-a(〜を持っている)という関係ですね。営業部レポートは、CSVフォーマッタを使う。HTMLフォーマッタを使う。でも、営業部レポート自体がCSVの一種だとか、HTMLの一種だとか——そういうことではないんです」

 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
package DepartmentReport;
use v5.36;

sub new ($class, %args) {
    die "formatter is required" unless $args{formatter};
    return bless {
        formatter => $args{formatter},
    }, $class;
}

sub formatter ($self) { $self->{formatter} }

# サブクラスでオーバーライド: 部門固有のデータ集計
sub aggregate ($self, $data) { die "not implemented" }

# 共通のレポート生成ロジック
sub generate ($self, $data) {
    my $metrics = $self->aggregate($data);
    my $title   = $self->title;

    my $output = $self->formatter->render_header($title);
    for my $pair ($metrics->@*) {
        $output .= $self->formatter->render_row(
            $pair->[0], $pair->[1]
        );
    }
    $output .= $self->formatter->render_footer;

    return $output;
}

sub title ($self) { die "not implemented" }

1;

「各部門は、この DepartmentReport を継承して、自分だけのデータ集計ロジックを書きます」

 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
package DepartmentReport::Sales;
use v5.36;
use parent 'DepartmentReport';

sub title ($self) { '営業部レポート' }

sub aggregate ($self, $data) {
    my $total_sales = 0;
    my ($deals_won, $deals_total) = (0, 0);

    for my $row ($data->@*) {
        $total_sales += $row->{amount} // 0;
        $deals_won   += $row->{won}    // 0;
        $deals_total += $row->{total}  // 0;
    }

    my $win_rate = $deals_total > 0
        ? sprintf("%.1f%%", $deals_won / $deals_total * 100)
        : "0.0%";

    return [
        ['売上合計', $total_sales],
        ['成約率',   $win_rate],
    ];
}

1;

「使うときは、こう組み合わせるだけです」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 営業部 × CSV
my $report = DepartmentReport::Sales->new(
    formatter => ReportFormatter::CSV->new,
);
print $report->generate(\@sales_data);

# 営業部 × HTML ——formatter を差し替えるだけ
my $report_html = DepartmentReport::Sales->new(
    formatter => ReportFormatter::HTML->new,
);
print $report_html->generate(\@sales_data);

画面を覗き込んだ。あの200行の関数が消えていた。代わりに、それぞれ20行足らずのファイルが並んでいる。

「……新しい部門が増えたら、部門クラスを1つ追加するだけ?」

ナナコさんが頷いた。

「はい。出力形式側を触る必要はありません」

「新しい形式が増えても……」

「形式クラスを1つ追加するだけです。部門側は何も変わりません」

胃の底にずっと居座っていた鈍い痛みが、すうっと引いていくのを感じた。

「胃が……痛くならない……」

術後経過

先生が黙ってキーボードを叩いた。テストが走る。全てのチェックマークが緑色に変わった。

「……試してみろ」

先生が椅子を横にずらし、キーボードの前を空けた。

私は恐る恐る座って、新しいファイルを作り始めた。広報部——来月追加される予定だった、あの部門だ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package DepartmentReport::PR;
use v5.36;
use parent 'DepartmentReport';

sub title ($self) { '広報部レポート' }

sub aggregate ($self, $data) {
    my $articles   = 0;
    my $page_views = 0;

    for my $row ($data->@*) {
        $articles   += $row->{articles}   // 0;
        $page_views += $row->{page_views} // 0;
    }

    return [
        ['掲載記事数',   $articles],
        ['総ページビュー', $page_views],
    ];
}

1;

ファイルを1つ作って、テストを1つ書いて、実行する。——通った。

以前なら、3箇所のif文にそれぞれ分岐を追加して、他の分岐を壊していないか全部のパターンを手動で確認して、それでも月曜の朝にバグ報告が来て——。

今は、ファイルを1つ追加するだけだ。

私は思わず深呼吸をした。久しぶりに、月曜の朝が怖くない気がした。

帰り支度をしながら、カウンターに置いてあったウェットティッシュで手を拭こうとした。すると、先生が椅子に座ったまま、片手をすっとこちらに伸ばした。ウェットティッシュの箱を、私のほうにずらすように。

——この人、不愛想なのに、さりげなく気遣いができるんだ。

そう思って受け取ろうとした瞬間、先生はすでにモニタに向き直っていた。よく見ると、ティッシュの箱があった場所にはキーボードが移動している。単に邪魔だったから退かしただけだったのだ。

ナナコさんが、何も言わずに、やれやれという顔で微笑んでいた。

鉄扉のほうへ歩きかけたとき、背後からぼそりと声がした。

「……命名。直せ」

振り向くと、先生はモニタに向かったまま微動だにしていない。

ナナコさんが少し申し訳なさそうに、でも確かな口調で言った。

「あの……変数名がExcelのセル番地のままなのは……やはり、治していただけると」

「……はい」

苦笑しながら答えた。

鉄扉を押して廊下に出る。磨かれたリノリウムの床を歩きながら、カバンの中のスプレッドシート——ハサミで切られた紙片——をそっと取り出した。

行と列。Abstractionと Implementation。ピボットテーブルの考え方。

……私がずっとExcelでやってきたことと、根っこは同じだったんだ。

蛍光灯がわずかに瞬き、私の影が伸びた。ノートPCの中にはまだ $cell_A1 が残っている。でも、それを直すのは——今の私にはもう、怖くない。


処方箋まとめ

症状適用すべき経過観察
2つ以上の独立した変化の軸がある
軸の組合せごとにクラスやif分岐が爆発している
片方の軸だけ変更したいのに他方も影響を受ける
変化の軸が1つだけで、単純な継承で十分
組合せの数が少なく、今後も増える見込みがない

治療のステップ

  1. 二軸の特定 — コードの中で独立に変化する2つの次元(抽象と実装)を見つける
  2. Implementation の分離 — 出力形式など「やり方」を担うインターフェースを定義する
  3. Abstraction の設計 — 部門ロジックなど「何をするか」を担う基底クラスを作り、Implementationをhas-aで保持する
  4. 具体クラスの実装 — 各軸ごとに具体的なクラスを作成する
  5. 組合せの注入 — コンストラクタで Abstraction に Implementation を渡し、自由に組み合わせる

助手より

ピボットテーブルの発想で Bridge を理解されたのは、実は素晴らしいことです。デザインパターンは「新しい何か」ではなく、多くの方がすでに日常で実践している考え方に名前をつけたもの。あなたのExcelの経験は、決して無駄ではありませんでした。

次に胃が痛くなったら……いつでもいらしてくださいね。

——ナナコ

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