Featured image of post コードドクター【Template Method】クローン増殖症候群〜共通カルテの処方〜

コードドクター【Template Method】クローン増殖症候群〜共通カルテの処方〜

来院

「まず手順書を作りましょう」

それが私の口癖だ。瀬川律子、40歳。大手メーカーの総務部で15年間、経費精算から有給申請まで、あらゆる社内申請の手順書を作成・管理してきた。「手順書の女王」などと社内で呼ばれていたのも、満更ではなかった。

紙とExcelで回していたフローをデジタル化するプロジェクトが立ち上がり、私はそのPMに抜擢された。要件定義だけではなく、「どうせなら自分で作ろう」と一念発起し、テキスト処理に強いと聞いた Perl を独学で学び始めた。

プログラミング歴3年。私のコードは、私の手順書と同じくらい几帳面だ。きっちりサブルーチンに分け、変数名の命名規則を統一し、コメントも豊富に入れた。

しかし——最近、何かがおかしい。

経費申請、有給申請、備品購入申請……。仕様書通りに作っているのに、コードがどんどん肥大化していく。1ヶ所——たとえば「通知メールのフッター」を直すだけで、10個のモジュールすべてを開いて同じ修正を繰り返さなければならない。

先月「慶弔見舞金申請」を追加した際、ついに事故が起きた。既存の経費申請モジュールをコピーして修正したのだが、承認ルートの書き換えを漏らし、部長承認が二重で走るバグを出してしまったのだ。

私のやり方、何か間違っているんでしょうか。

平日の昼休み。私は有給を取り、A4クリアファイル50枚綴りの「仕様書(手順書)」を抱えて、噂に聞いた「コード診療所」を訪れていた。

雑居ビルの2階。重そうな鉄の扉を引いて開けると、そこは私の手順書棚とは対極の世界だった。O’Reillyの鈍器のような本が壁を作り、マザーボードの山が床を侵食している。整理整頓という概念が存在しない空間だ。

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

奥のカウンターから、柔和な笑顔の女性——ナナコと名乗った助手が迎えてくれた。

「あの……予約は入れてなかったんですけど、社内システムのことでちょっと確認したくて」

私はあくまで「確認」に来たつもりだった。自分のやり方が根本から間違っているとは思いたくなかったのだ。

通された診察室には、トリプルディスプレイに背を向けた黒いジャケットを着た黒髪の男がいた。彼が振り返り、私を一瞥する。白衣は着ていないが、どこか医者を思わせる佇まいだ。何も言わない。

私は持参した分厚いクリアファイルをテーブルに置いた。表紙にはテプラで「社内申請システム マスター手順書」と貼ってある。

「これが仕様書です。まず全体像から説明しますね」

男——コードドクターは、私のクリアファイルには一切触れず、目の前の見慣れない小さなキーボードに手を伸ばした。

「コードは」

「あっ、はい。USBメモリに……」

触診

USBメモリを渡し、ドクターが私のコードを開いた。最初に ExpenseRequest.pm。スクロールしながら黙読していく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
sub process ($self) {
    # Step 1: 入力チェック
    $self->validate_input();

    # Step 2: 予算確認
    $self->check_budget();

    # Step 3: 承認ルート
    $self->route_approval();

    # Step 4: 通知
    $self->send_notification();

    # Step 5: 記録保存
    $self->archive_record();

    return $self->{status};
}

「……丁寧だ」

ドクターが短く呟いた。

私はホッとして少し微笑んだ。「やっぱり、サブルーチンに分けてるのが良かったんですかね。先輩に教わった通りに——」

ドクターは何も答えず、次に LeaveRequest.pm を開いた。また黙読。さらに EquipmentRequest.pm

彼の目が細くなった。3つのファイルを、トリプルディスプレイのそれぞれに並べて表示する。

「…… クローン だ」

「え?」

ナナコさんが、3つのディスプレイを指差した。

「瀬川さん、この3つのモジュール、処理の流れを比べてみてください。validate_inputcheck_budgetroute_approvalsend_notificationarchive_record——全部同じ順番ですよね?」

「はい、だって申請処理ってそういう流れですから……あっ」

「そうなんです。流れは同じで、違うのはバリデーションの中身と承認ルートだけ。でも、その『同じ流れ』が10本のモジュールにそれぞれ丸ごとコピーされています」

ドクターがターミナルで diff コマンドを打った。2つのモジュール間の差分が表示される。差異はわずか15行。残りの200行は完全一致していた。

「200行。無駄に同じ」

ドクターが容赦なく事実を突きつける。

「でも、申請の種類が違うから、ファイルを分けないと——」

「ファイルは分ける。流れは分けない」

「流れは……分けない?」

ナナコさんが、私の顔を覗き込んだ。

「瀬川さん、手順書をたくさん作ってこられたんですよね。たとえば経費申請と出張申請の手順書、共通する手順はどうしてましたか?」

「……共通する部分は、マスター手順書から参照する形にしてました。各手順書には『基本フローはマスターを参照』って書いて、差分だけ個別に書いてました」

答えた瞬間、沈黙が落ちた。

「あ……。コードで、同じことをしていない」

私は気づいてしまった。自分の得意分野であるはずの「手順の共通化」を、プログラミングの世界では全く実践していなかったことに。

「病名。 クローン増殖症候群

ドクターが宣告した。

「同じ遺伝子——つまり同じ処理フロー——を持つクローンが10体。でもマスターの原型がないので、1ヶ所治すと10体すべてを個別に直さないといけません。1体を治し忘れると、先月の慶弔見舞金申請のように、変異が悪化してバグになります」

ナナコさんの穏やかな声が、私の傷口に塩を——いや、的確な消毒薬を塗っていく。

3つのディスプレイに並んだクローンコードを指し示すドクターと、自分の失敗に気づいて愕然とする瀬川

外科手術

ドクターが無言でエディタに向かい、新しいファイルを作った。

AbstractRequest.pm

「これが骨格だ」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# === テンプレートメソッド ===
sub process ($self) {
    $self->validate_input();
    $self->check_budget();
    $self->route_approval();
    $self->send_notification();
    $self->archive_record();

    return $self->{status};
}

# === 抽象メソッド(サブクラスで必ず実装) ===
sub validate_input ($self) { ... }
sub route_approval ($self) { ... }

「マスター手順書ができました。流れだけが書いてあって、中身はまだ空です」

ナナコさんの解説を聞くまでもない。私にはこの構造が痛いほどよくわかった。「基本フローは私に従え。細かいルールはお前たちが決めろ」。私が15年間、紙の手順書でやってきたことそのものだ。

「次。あなたが書け」

ドクターが椅子を立ち、私に場所を譲った。

「えっ、私が?」

「先生の言う通りです。バリデーションのルールは瀬川さんが一番詳しいですよね。この骨格を使って、経費申請(ExpenseRequest)を書いてみてください。変わる部分だけで大丈夫ですよ」

私は恐る恐る、その見慣れないキーボードに手を乗せた。スコスコという打鍵感が妙に心地よい。

ExpenseRequest.pmAbstractRequest のサブクラス(子)として宣言し、空だった validate_inputroute_approval だけを実装していく。金額のチェック、カテゴリの検証、課長と部長の承認ルート。

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

sub validate_input ($self) {
    die "金額は正の数である必要があります" unless $self->{amount} > 0;
    # ...カテゴリチェック...
    return 1;
}

sub route_approval ($self) {
    push $self->{approved_by}->@*, '課長';
    push $self->{approved_by}->@*, '部長' if $self->{amount} >= 10_000;
    $self->{status} = 'approved';
    return 1;
}
1;

差分はこれだけだった。200行あったコードが、たった数十行になった。処理の流れは、親である AbstractRequest がすべて引き受けてくれているからだ。

ドクターが斜め後ろからモニタを見下ろした。

「フック。砂糖を入れるか聞け」

「フック……? 砂糖?」

ナナコさんが少しだけ申し訳なさそうに補足した。

「ここはちょっとだけ専門用語ですね。申請の中には『上長コメントが必須の場合』と『任意の場合』がありますよね? そういう『あってもなくてもいい手順』をフックメソッドと呼ぶんです。基本は何もしないけど、必要な申請だけ上書きする——コーヒーに砂糖を入れるかどうかみたいなものです」

私はハッとした。

「あ、慶弔見舞金は上長コメントが必須で、経費精算は任意だから……ここにフックを入れれば——」

私は基底クラスの process() メソッドの流れに、一行を付け加えた。

1
2
3
4
    # フック: 上長コメントが必要な申請だけオーバーライド
    if ($self->requires_manager_comment()) {
        $self->request_manager_comment();
    }

基底クラスでは requires_manager_comment() は単に 0(False)を返すようにしておく。慶弔見舞金のサブクラスでだけ、これを 1(True)を返すようにオーバーライド(上書き)するのだ。

「……いい」

ドクターがモニタから目を離さずに短く呟いた。

テストを実行する。5種類の申請が、すべて正しく、完璧に処理された。

術後経過

「先生、あの……」

私は興奮を抑えきれずに言った。

「先月バグを出した慶弔見舞金申請、今の構造で追加してみていいですか?」

「やれ」

私は CondolenceRequest.pm を新規作成した。親クラスを継承し、バリデーションと承認ルート、そして先ほどのフックメソッドを 1 にする処理だけを書く。

既存のモジュールには、1文字たりとも触れていない。バグを埋め込む余地そのものがないのだ。

テストを実行する。

1
2
3
4
5
ok 1 - 慶弔見舞金申請は承認される
ok 2 - 慶弔は3段階承認
ok 3 - フックメソッドにより上長コメント必須
ok 4 - 経費申請は上長コメント不要
ok 5 - 慶弔申請は上長コメント必要

「1ファイル……たった1ファイル追加しただけ。前回は3日かかったのに……」

「マスター手順書と同じです。新しい申請を追加するとき、基本フローのマスターは触りませんよね。差分だけ書けばいいんです」

ナナコさんの言葉に、私は深く頷いた。

ドクターが席を立った。彼がテーブル上の私のクリアファイルを手に取った。分厚い、50枚綴りの仕様書。彼は一瞬パラパラとめくり——そして、クリアファイルの角をきっちりと揃えて、テーブルに戻した。

(あ……)

私は息を呑んだ。無造作にコードの異常だけを見抜いていたあの人が、私の手順書を手に取った。しかも、角を揃えて戻してくれた。

15年間の私の仕事を、認めてもらえた——ような気がした。きっと気のせいだろうけど。

「先生、手順書が気になりましたか?」

ナナコさんが尋ねた。

「紙が反っていた」

ドクターの答えは素っ気ない。

「……ですよね」ナナコさんが私に小さく苦笑した。「先生は、整理整頓だけは得意なんです」

私は部屋を見渡した。マザーボードの山、乱雑に積まれた鈍器のような本たち。整理整頓? この部屋のどこが? と喉まで出かかったが、不思議と悪い気はしなかった。

「感謝は、このコードに」

「……っ、はい!」

私は弾かれたように返事をした。そして鞄を持ち、テーブルの上のクリアファイルを両手でしっかりと抱え直す。

「あの、本当にありがとうございました! これまで15年間ずっとやってきたこと……プログラミングでも無駄じゃなかったって、そう思えました」

ドクターはもうモニタに向かっていて、振り返らなかった。それでも構わなかった。私はその黒い背中に向かって、深く、何度も頭を下げた。

鉄の扉を押して開け、廊下に出る。

ふと立ち止まり、クリアファイルの表紙を見つめた。「社内申請システム マスター手順書」。

私はもう、答えを持っていたのだ。15年間ずっと手順書で実践していたことを、コードに翻訳するだけでよかった。

マスター手順書 = AbstractRequest。 各申請の差分手順書 = サブクラス。

テンプレートメソッド。 テンプレートという名前が、今ならしっくり来る。同じ型(テンプレート)を使い回せば、もうクローンが増殖することはない。

帰ったら、まず10本のクローンをこのテンプレートに統合しよう。

手順書のマスター参照方式の威力を、もう一度コードで証明してやる。


処方箋まとめ

症状適用すべき経過観察
複数のクラスで処理の「流れ(順序)」がほぼ同じ
構造は同じだが、特定のステップだけ実装が異なる
アルゴリズム全体を丸ごと差し替えたい(Strategyの領域)
「必要かもしれない」という理由で無数のフックメソッドが生えている

治療のステップ

  1. 共通フローの抽出: 複数のクローンコードから、完全に一致する処理の「骨格(流れ)」を見つけ出す。
  2. 基底クラスの実装: 抽出した骨格を、基底(親)クラスの「テンプレートメソッド(process など)」として定義する。
  3. 抽象メソッドの定義: サブクラスごとに実装が変わるステップ(validate_input など)を洗い出し、基底クラスでは空(または例外を投げる形)にしておく。
  4. フックメソッドの提供: サブクラスによっては追加処理が必要になる箇所に、「デフォルトでは何もしない」フックを仕込む。
  5. サブクラスの分化: 個別のクラスを作成し、基底クラスを継承させ、変わる部分(抽象メソッド/フック)だけを実装する。

助手より

瀬川さん、本日はご来院ありがとうございました。「手順書の女王」と呼ばれる瀬川さんが、最初からマスター手順書の発想をお持ちだったこと、先生も(口には出しませんが)驚いていたと思いますよ。

人間が紙の手順書で行う「ルール化と例外管理」は、プログラムの設計思想と驚くほど共通しています。今回の Template Method パターンは、まさに瀬川さんの長年のご経験がコードの世界で花開いた瞬間でした。どうか自信を持って、残りのクローンたちの統合手術を進めてくださいね。

——ナナコ

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