Featured image of post コードガーデナー【Template Method】株ごとにコピーした手順、気づけば水やりの順番までずれていた〜骨格は一つに決め、差分だけを預ける〜

コードガーデナー【Template Method】株ごとにコピーした手順、気づけば水やりの順番までずれていた〜骨格は一つに決め、差分だけを預ける〜

ハーブごとにコピペした開店準備の手順が、品種を増やすたびに少しずつずれ、事故未遂を起こす。庭師が教えるのは、トレイトのデフォルトメソッドで手順の骨格を固定するTemplate Methodだ。

春の朝、シャッター通りを小走りで

脱サラして小さなハーブティー専門店を開いて、もうすぐ3年になる。前は飲食チェーンで店舗運営をしていたが、いつか自分の店を持ちたくて独立した。開業当初はミントとカモミールの2品種だけを小さな鉢で育てていたのが、口コミで少しずつ評判が広がり、レモンバームを皮切りに品種を増やしてきた。今では棚に並ぶ鉢の数も、開業当時とは比べものにならない。

毎朝の開店準備で、鉢ごとの世話——水やり、日照確認、収穫判定——をRustで書いた自作のスクリプトで管理している。プログラミングの経験自体は前職の頃からあったので、ちょっとした自動化くらいはできる。品種を増やすたび、既存の品種の関数をコピーして名前だけ変え、細かいところを手直ししてきた。動けばいい、という気持ちでやってきたので、手順の並びそのものを見直したことは、一度もなかった。

今朝、それが牙を剥いた。

開店準備の途中、カモミールの株から萎れかけた葉を摘みそうになった。スクリプトの画面には「収穫可」の表示が出ていて、私はいつも通りそれを信じて鋏を入れかけた。声をかけてくれたのはアルバイトのスタッフだった。「これ、ちょっと萎れてませんか」——その一言で手を止めなければ、そのままお客の前に出ていたかもしれない。摘んだ葉を陽の下で見返すと、たしかに端が少し茶色くなっている。危なかった、では済まされない。心臓がまだ少し速いままだ。

春の朝、店のシャッターはまだ下りたままだ。私は小走りで商店街を抜け、庭師の工房へ向かう。開店準備の途中で店を空けてきたことが気になって仕方ない。息が上がる。道すがら、工房の入口の一角に古い木が立っているのは知っているが、今日はそれを気に留める余裕がまったくなかった。急いでいたことしか、あとになっても思い出せない。

工房の戸を開けると、庭師さんはちょうど鉢の位置を直していた手を止め、こちらを見ないまま、開いたノートパソコンの方へ顎を小さく向けた。座る間もなく画面を差し出す。

「今朝、カモミールの葉、萎れかけたのに気づかず摘みそうになったんです。スタッフが止めてくれたから良かったものの……」

言葉が普段より速く出ているのが、自分でも分かった。画面には、開店準備スクリプトのソースコードが出ている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Chamomile {
    soil_moisture: u8,
    is_in_sun: bool,
    leaf_condition: LeafCondition,
}

fn water_chamomile(c: &mut Chamomile) {
    c.soil_moisture = 70;
}

fn check_sunlight_chamomile(c: &mut Chamomile) {
    c.is_in_sun = true;
}

fn judge_harvest_chamomile(c: &Chamomile) -> bool {
    c.is_in_sun && c.leaf_condition == LeafCondition::Fresh
}

fn open_chamomile(c: &mut Chamomile) {
    water_chamomile(c);
    let _ = judge_harvest_chamomile(c);
    check_sunlight_chamomile(c);
}

見比べられるように、ミントの方の関数も一緒に開いておく。こちらは、最初に書いたときのまま手を入れていない。

1
2
3
4
5
fn open_mint(m: &mut Mint) {
    water_mint(m);
    check_sunlight_mint(m);
    let _ = judge_harvest_mint(m);
}

症状観察 ── 庭師の沈黙

庭師さんは画面に顔を近づけ、open_mintopen_chamomile、2つの関数の3行の並びを、視線だけで何度も往復させた。台詞はほぼない。工房の奥では、まだ蕾の固い春の鉢が窓際に並んでいて、外の商店街のざわめきが遠く聞こえる。

「品種ごとに関数は分けてるので、それぞれちゃんと動くはずなんですが」

私はそう付け加えたが、庭師さんは何も言わず、open_chamomileの2行目と3行目の並びの前で、喉がほんの小さく動いた。それだけだった。手も、視線も、そこから動かない。

何を見ているのか、私には分からない。ただ、その一点で何かが引っかかっていることだけは伝わってくる。じっと待つ数秒が、実際より長く感じられた。

「もしかして、私、何か変な書き方しちゃってますか」

たまらず聞くと、短い沈黙のあと、庭師さんが口を開いた。

「順番が、1つだけ違う」

それだけだった。どこの、何の順番なのかは言わない。

見立てと翻訳 ── 見習いの通訳

庭師さんの視線がopen_chamomileの3行で止まった瞬間、見習いのヒバリさんはいつものように口を開きかけた。でも、途中で思い直したように黙り、画面の3行——water_chamomilejudge_harvest_chamomilecheck_sunlight_chamomileの並びを、指でゆっくりなぞった。

私は自分の目でその指の動きを追った。上から下へ、一行ずつ。

「……あ」

声が漏れた。

「これ、2行目と3行目、逆じゃないですか。日照確認より先に、収穫判定してる」

二つの鉢を並べて見せられると、違いは一目瞭然だった。ミントの手順は上から下まで、迷いなく一直線。カモミールだけ、真ん中の二段が入れ替わっている。

open_mintは水やり→日照確認→収穫判定の正しい順、open_chamomileは日照確認と収穫判定の順序が入れ替わっている比較図

ヒバリさんはそこで初めて小さく頷いた。少しだけほっとしたような顔をしている。

「今日は、先に言っちゃうより、ご自分で見つけてもらった方がいい気がして」

普段のヒバリさんなら、庭師さんの一言を受けてすぐに言葉を継ぐ人だ。今朝の私がよほど動揺していたからか、あるいは何か思うところがあったのか、珍しく言葉を保留していたらしい。

「で、これ、open_mintの方は正しい順なんですよね。ということは……」

「レモンバームを足したとき、カモミールの関数をコピーしたんです。その時、うっかり順番を入れ替えて書いちゃったのかも」

経緯を話しながら、記憶をたどる。「最初はミントとカモミールの2つだけでした。レモンバームを置くようになったとき、ミントの関数をコピーして名前だけ変えたんです。動いてさえいれば、それで十分だと思ってました」

ヒバリさんが、その先を引き取るように言う。

「品種が増えるたびに、この3行の並びを毎回コピーして、毎回正しく守り続けるのって、けっこう大変じゃないですか? 一箇所だけ、絶対に順番を間違えない場所を作っておけたら」

少し考えて、思いついたことを口にしてみる。

「それって、手順ぜんぶ、まとめて誰かに任せちゃうとか、できません?」

「それ、Strategyという考え方に近いですね」

ヒバリさんが答える。

「でも、それだと任せた先——手順ぜんぶを引き受けたその1つのメソッドの中で、またwatercheck_sunlightjudge_harvestの順を自分で書くことになりますよね。結局、書く場所が変わるだけで、順番を守る責任は誰かの手元に残ったままです」

なるほど、と思う。関数を分けても、コピーする作業そのものは消えない。移動しただけだ。

Rustにはtraitという仕組みがある。共通の振る舞いを型ごとに実装させる、という程度には前に何かで聞いたことがあったが、ちゃんと使ったことはなかった。

「順番そのものを、品種ごとにコピーするんじゃなくて、1箇所にまとめて固定しておく——それが今回の手入れです」

庭師さんが短く言った。

Template Method。デザインパターンの一つで、手順の骨格をあらかじめ固定しておき、品種ごとに違う部分だけを差し込み口として開けておく考え方だという。私にはまだ実感が湧かないが、とにかく試してみるしかない。

まず、is_in_sunが更新される前にjudge_harvest_chamomileが呼ばれることで、前日の状態のまま収穫可否が決まってしまう——という誤判定を、テストコードで再現してみることにした。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#[test]
fn open_chamomile_misjudges_harvest_using_stale_is_in_sun() {
    let mut c = Chamomile {
        soil_moisture: 0,
        is_in_sun: false, // 前日の状態
        leaf_condition: LeafCondition::Fresh,
    };
    let misjudged = judge_harvest_chamomile(&c);
    assert!(!misjudged, "更新前のis_in_sun=falseでは収穫不可と判定されるはず");

    open_chamomile(&mut c);
    assert!(c.is_in_sun, "open_chamomile完了後はis_in_sunがtrueになる");
    assert!(!misjudged, "しかし収穫可否はis_in_sun更新前の値で既に確定していた");
}

たしかに、今朝起きたことがそのまま再現されている。is_in_sunが更新される、そのわずか一瞬前に、収穫可否がもう確定してしまっている。画面の中の小さな一行が、今朝の私の手元まで直結していたのだと思うと、少し背筋が冷える。

手入れ ── 剪定と接ぎ木

庭師さんはキーボードに手を伸ばし、まずトレイトの定義から書き始めた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
trait HerbCare {
    fn water(&mut self);
    fn check_sunlight(&mut self);
    fn judge_harvest(&self) -> bool;

    // 骨格:水やり→日照確認→収穫判定の順序はここに一度だけ書く。
    // 各品種はこのメソッドに触れない、という運用を前提にする
    fn care(&mut self) -> bool {
        self.water();
        self.check_sunlight();
        self.judge_harvest()
    }
}

watercheck_sunlightjudge_harvestの3つは、まだ中身のない「差し込み口」だ。そしてcareだけが、その3つを呼ぶ順番を持っている。書きながら庭師さんが小さく付け加える。

「順番は、ここに一度だけ書く」

 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
struct Mint {
    soil_moisture: u8,
    is_in_sun: bool,
    leaf_condition: LeafCondition,
}

impl HerbCare for Mint {
    fn water(&mut self) {
        self.soil_moisture = 80;
    }
    fn check_sunlight(&mut self) {
        self.is_in_sun = true;
    }
    fn judge_harvest(&self) -> bool {
        self.is_in_sun && self.leaf_condition == LeafCondition::Fresh
    }
}

struct Chamomile {
    soil_moisture: u8,
    is_in_sun: bool,
    leaf_condition: LeafCondition,
}

impl HerbCare for Chamomile {
    fn water(&mut self) {
        self.soil_moisture = 70;
    }
    fn check_sunlight(&mut self) {
        self.is_in_sun = true;
    }
    fn judge_harvest(&self) -> bool {
        self.is_in_sun && self.leaf_condition == LeafCondition::Fresh
    }
}

ヒバリさんが手元を見ながら言葉にする。

careは、もうここにしかないんです。MintChamomileも、implの中ではwatercheck_sunlightjudge_harvestしか書いていなくて、care自体は再実装していません。だから呼ばれるときは、必ずトレイト側に書いたこの1つのcareが使われます」

3つのメソッドの中身——水やりの量、日照確認、収穫判定のロジック——は、open_mintopen_chamomileにあったものとまったく同じだ。変わったのは、それを「どの順で呼ぶか」を誰が決めるか、という一点だけらしい。

私は自分でも書いてみることにした。レモンバームを新しい品種として追加する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct LemonBalm {
    soil_moisture: u8,
    is_in_sun: bool,
    leaf_condition: LeafCondition,
}

impl HerbCare for LemonBalm {
    fn water(&mut self) {
        self.soil_moisture = 75;
    }
    fn check_sunlight(&mut self) {
        self.is_in_sun = true;
    }
    fn judge_harvest(&self) -> bool {
        self.is_in_sun && self.leaf_condition == LeafCondition::Fresh
    }
}

画面の中では、careという一本の支柱から三つの差し込み口が伸びていて、ミント・カモミール・レモンバーム、三つの鉢がそれぞれの中身だけをそこに挿している。骨格は一本。鉢の数だけ増えるのは、いつも同じ三つの穴を埋める作業だけだった。

trait HerbCareのcare()が骨格として3つの差し込み口(water/check_sunlight/judge_harvest)へつながり、Mint・Chamomile・LemonBalmの3品種がそれぞれ実装する関係図

watercheck_sunlightjudge_harvestの3つを書いただけで、careには一切触れていない。書き終えて画面を見返し、思わず声が出た。

「本当に、これだけで足りるんですか」

拍子抜けしたように呟くと、庭師さんは手元の土を払うような仕草のまま、小さく頷いた。

care自体を書き換えようと思えば、書き換えられてしまいます。順番を守れる仕組みというより、順番を守る場所を1つに決めた、という話です」

その一言に、少し引っかかった。絶対に安全というわけではないらしい。でも、少なくとも、品種を増やすたびに順番そのものを書き写す作業は、もう発生しない。責任の置き場所が変わったのだと、なんとなく分かる。

「これから先、順番自体が品種によって違う、みたいなことは起きませんか」

聞いてみると、ヒバリさんが答えた。

「今のところ、水やり→日照確認→収穫判定という順番を変える理由は、どの品種にもないですよね。もし本当に順番ごと変えたい品種が出てきたら、そのときはまた別の手入れを考えることになると思います」

なるほど、と思う。今回のcareは「順番はどの品種でも同じ」という前提の上に立っている。その前提が崩れたら、この手入れもそのままでは効かなくなるということらしい。

呼び出し順序が本当に固定されているか、テストで確かめてみることにした。

 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
struct OrderTrackingHerb {
    is_in_sun: bool,
    leaf_condition: LeafCondition,
    log: Rc<RefCell<Vec<&'static str>>>,
}

impl HerbCare for OrderTrackingHerb {
    fn water(&mut self) {
        self.log.borrow_mut().push("water");
    }
    fn check_sunlight(&mut self) {
        self.log.borrow_mut().push("check_sunlight");
        self.is_in_sun = true;
    }
    fn judge_harvest(&self) -> bool {
        self.log.borrow_mut().push("judge_harvest");
        self.is_in_sun && self.leaf_condition == LeafCondition::Fresh
    }
}

#[test]
fn care_calls_water_then_check_sunlight_then_judge_harvest_in_order() {
    let log = Rc::new(RefCell::new(Vec::new()));
    let mut herb = OrderTrackingHerb {
        is_in_sun: false,
        leaf_condition: LeafCondition::Fresh,
        log: Rc::clone(&log),
    };
    herb.care();
    assert_eq!(
        *log.borrow(),
        vec!["water", "check_sunlight", "judge_harvest"],
        "careは3ステップをこの順で1回ずつ呼ぶはず"
    );
}

watercheck_sunlightjudge_harvest。記録されたログは、ちゃんとこの順になっている。レモンバームを足しても、careのコードそのものは一文字も変わっていない。

「品種が増えるたび私が書くのは、もう中身の3つだけなんですね。順番を決めている場所は、最初からずっと同じところにある」

自分の言葉でそう言い直してみると、ようやく腑に落ちた気がした。今朝のあの一瞬——is_in_sunが更新される前に判定が確定してしまった、あの並びの入れ替わり——は、もう起こりようがない。少なくとも、順番をコピーし直す作業がある限りは。

見送り ── 次の季節へ

一通りの手入れを見届け、庭師さんが短く一言を残した。

見送りの言葉の代わりに、私は最後にもう一度尋ねた。

「レモンバームの次に何か増えても、私が書き直すのは、また同じ3つだけでいいんでしょうか」

庭師さんは、それには答えず、ただ小さく頷いた。

工房を出るとき、私は庭師さんについて再び入口の脇を抜けた。今日は庭師さんが古い木に一切触れず、ただ横を通り過ぎるだけだった。特に意味はなさそうだったけれど、なぜか「今日は、何もしないんだ」という何でもない事実が、私の中に妙に残った。

私は急ぎ足で商店街へ戻る。開店時間が迫っている。今朝の動揺は、まだ完全には収まっていない。それでも、手元のノートパソコンの重さが、来たときより少しだけ軽く感じられた。


手入れ記録

こんな症状が出たら入れるべき手入れ(パターン)まだ様子見でいい
似た処理を品種・種類ごとにコピーして書いており、手順の呼び出し順序が個体ごとに少しずつずれている✓ Template Method(トレイトのデフォルトメソッドで骨格を固定)
手順は複数あるが、呼び出す順番自体は1箇所にしか存在せず、コピーで増えているのは中身の実装だけ✓ 現状のまま様子見

手入れの手順

  1. コピーで増えている複数の処理から、共通して守られるべき「手順の順番」を洗い出す(今回は水やり→日照確認→収穫判定)
  2. 共通のトレイトを定義し、順番を含む骨格をそのデフォルトメソッド(今回はcare)として1箇所に書く
  3. 品種・種類ごとに異なる部分だけを、トレイトの必須メソッド(差し込み口)として切り出す
  4. 各品種の構造体に、必須メソッドだけを実装する。骨格メソッドには触れない
  5. 新しい品種を追加したとき、骨格メソッドのコード自体には一切手を加えていないことをテストで裏付ける

庭師の一言

揃う。芽の出し方を、ひとつに決めておけば。

見習いの手控え

今日は、言葉より先に指を動かしてみました。ちゃんとご自分で気づいてもらえて良かったです。順番を守る場所を1つに決めておけば、増やす側は中身のことだけ考えていられるんですね。

comments powered by Disqus
システム開発・AIワークフローのご相談は Meetsource
Hugo で構築されています。
テーマ StackJimmy によって設計されています。