Featured image of post コードガーデナー【Facade】土壌・灌漑・施肥、正しい順番を知るのは先代だけだった〜複雑な手順は、窓口の奥にしまっておく〜

コードガーデナー【Facade】土壌・灌漑・施肥、正しい順番を知るのは先代だけだった〜複雑な手順は、窓口の奥にしまっておく〜

土壌診断・灌漑制御・施肥計画、正しい呼び出し順序は先代の頭の中にしかなかった。庭師が教えるのは、順序制御だけを一つの窓口に閉じ込めるFacadeだ。

秋の午後、タクシーを飛ばして

三代続く野菜農園を、法人化したのは十年ほど前になる。私はその三代目で、今年から正式に経営を引き継いだ。新卒で入ったIT企業でシステムエンジニアとして五年ほど働いたあと家業に戻り、しばらくは先代の右腕として働いていたが、代替わりしてからはまだ半年も経っていない。

今は収穫期の真っ只中で、農園全体が朝から慌ただしい。出荷用のトラックが列をなし、選果場からは無線のやり取りが絶え間なく聞こえてくる。そんな中、新しく雇ったスタッフ二人に向けて、私は昨夜遅くまで研修資料を書いていた。土壌診断、灌漑制御、施肥計画——この三つの管理ツールの使い方を説明する項目まで来て、手が止まった。「正しい順番で」としか書けなかった。何が正しい順番なのか、自分の言葉で説明できなかったからだ。

出荷の合間を縫って、私は事務所の前でタクシーを拾った。運転手に行き先を告げると、「ああ、あの木のあるとこですね」と軽く返される。車窓には収穫を終えたばかりの畑が流れていく。自分で運転する余裕さえない忙しさに、少し情けなくなる。

工房の入口には見慣れた古い木が立っているはずだが、今日は視界の端をただ通り過ぎていっただけだった。頭の中はずっと、さっき打ち切ってきた研修資料のことでいっぱいだった。

工房に着くと、庭師さんは挨拶もそこそこに、私が広げたノートパソコンの画面へ視線を向けた。息が整う前に、私は先に口を開いていた。

「先代のやり方、そのまま覚えるしかないんでしょうか」

自分で言っておきながら、少し驚いた。もっと落ち着いた切り出し方をするつもりだったのに、思いのほか言葉が先に出てしまった。

「新しく雇ったスタッフに、うちの管理システムの使い方を教えようとしてるんです。土壌の状態を診てから、水をやって、それから肥料をやる——先代の代からずっとそういう順番でやってきたんですが、いざマニュアルにしようとしたら、その順番がどこにも書かれていないことに気づいて」

説明しながら、自分でも早口になっているのが分かる。「土がまだ湿ってるうちに肥料をやると、根が傷むんです。水はけの悪い区画だと、それだけで一区画分の収穫が落ちることもあって……先代は感覚でそれを避けてたんだと思うんですけど、私にはその感覚がまだ無くて」

画面には、うちで使っている管理システムのソースコードが出ている。

 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
pub enum SoilStatus {
    Normal,
    Dry,
    Saturated,
}

pub struct SoilDiagnostics {
    pub moisture_level: u8,
}

impl SoilDiagnostics {
    pub fn diagnose(&self) -> SoilStatus {
        if self.moisture_level > 80 {
            SoilStatus::Saturated
        } else if self.moisture_level < 20 {
            SoilStatus::Dry
        } else {
            SoilStatus::Normal
        }
    }
}

pub struct IrrigationControl {
    pub flow_rate: u8,
}

impl IrrigationControl {
    pub fn irrigate(&mut self) {
        self.flow_rate = 50;
    }
}

pub struct FertilizationPlan {
    pub nutrient_mix: u8,
}

impl FertilizationPlan {
    pub fn apply(&mut self) {
        self.nutrient_mix = 30;
    }
}

土壌診断、灌漑制御、施肥計画。それぞれ独立した仕組みとして動く。「先代は、いつも土を見てから水をやって、それから肥料でした。何でその順なのか聞いたことはなかったんですけど、それで何十年もやってこられたので」

呼び出す側のコードも一緒に見せる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn veteran_routine(
    soil: &SoilDiagnostics,
    irrigation: &mut IrrigationControl,
    fert: &mut FertilizationPlan,
) {
    match soil.diagnose() {
        SoilStatus::Saturated => {
            // 過湿時は灌漑も施肥もスキップ
        }
        SoilStatus::Dry | SoilStatus::Normal => {
            irrigation.irrigate();
            fert.apply();
        }
    }
}

先代が個人でやっていた通りの順番だ。土壌を診断して、過湿なら何もせず、そうでなければ灌漑してから施肥する。私はこの関数を見つけるまで、そんな分岐があること自体、知らなかった。

症状観察 ── 背筋が、わずかに伸びる

庭師さんはコードをスクロールする指だけを動かした。三つの構造体が別々に並んでいる様子と、veteran_routineの分岐を、何度か目で追う。ほとんど声を出さない。工房の奥では、収穫を終えた鉢がいくつか片付けを待っていて、外からは軽トラのエンジン音が遠く聞こえていた。

「動くには動くんです、順番さえ合ってれば」

私はそう付け加えたが、庭師さんは何も言わず、もう一つの呼び出し方——新人スタッフが最初に書いたコードへと画面を進めた。

1
2
3
4
5
6
7
8
9
fn new_hire_routine(
    soil: &SoilDiagnostics,
    irrigation: &mut IrrigationControl,
    fert: &mut FertilizationPlan,
) {
    fert.apply(); // 土壌の状態を確認しないまま施肥してしまう
    irrigation.irrigate();
    let _ = soil.diagnose();
}

二つの関数を見比べた瞬間、庭師さんの背筋がわずかに伸びた。その一瞬で、何かが結論づいたらしい。理由までは、まだ教えてもらえない。

「これ、何か変えた方がいいんでしょうか」

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

「呼ぶ人によって、順番が違う」

庭師さんはそう言ったきり、また画面へ視線を戻した。どちらが正しいのかは、まだ教えてくれない。

見立てと翻訳 ── 「知ってる人だけが、正しく呼べる」

いつもなら庭師さんの言葉を受けてすぐに言葉を継ぐヒバリさんが、今日は先に私の方を向いた。

「それ、いつも同じ順番でやってます?」

不意を突かれて、少し考える。

「……先代はいつも決まった順でしたけど、新しく入った子たちは、たぶんバラバラです」

自分の言葉にしてみて、初めてそのことの重さに気づいた。

ヒバリさんが頷く。「それです。正しい順番を知ってる人だけが、たまたま正しく呼べてるだけなんです」

veteran_routineは、先代がずっと守ってきた正しい順番そのものだ。でもそれは、コードとして誰かに強制されているわけではない。先代の頭の中にしかない習慣を、たまたま先代が毎回正確に再現していただけだった。

「それって、マニュアルを整備すればいい話じゃないんですか」

経営者としての癖で、まずそう言ってみる。ヒバリさんは少し困ったように笑った。

「書いても、忙しいと読まれないこともあります。読んでも、間違って覚えることもあります。コードの方に、正しい順番の通り道を一本作っておけたら、そもそも覚える必要自体がなくなるんです」

言われてみれば、マニュアルは読む人の努力に頼る仕組みだ。コードの方に道を作っておけば、読む・読まないという段階自体が要らなくなる。

「複雑なサブシステム群の前に、単純な窓口を一つ置く。中の三つが何をしているかを知らなくても、その窓口さえ通れば正しい順番で動く——Facadeという定石です」

庭師さんが短く言った。

Facade。日本語にすると「窓口」や「正面口」といった意味らしい。土壌診断、灌漑制御、施肥計画という三つの複雑な仕組みの手前に、単純な入り口を一つ置く。言葉だけ聞くと、あっけないほど単純な話に思える。それでうちの新人研修が本当に楽になるのか、正直まだ半信半疑だった。

まず、新人スタッフの順番で今朝と同じことが起きることを、テストコードで確かめてみることにした。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#[test]
fn new_hire_routine_fertilizes_even_when_soil_is_saturated() {
    let soil = SoilDiagnostics { moisture_level: 90 };
    let mut irrigation = IrrigationControl { flow_rate: 0 };
    let mut fert = FertilizationPlan { nutrient_mix: 0 };

    new_hire_routine(&soil, &mut irrigation, &mut fert);

    assert_eq!(
        fert.nutrient_mix, 30,
        "過湿のはずなのに、順序が守られていないため施肥が実行されてしまう"
    );
}

土壌が過湿の状態でも、新人スタッフの順番で呼ぶと施肥が実行されてしまう。診断結果を確認する前に、もう施肥が終わっている。うちのスタッフが、まさにこの順番で困っていたわけではないけれど、いずれ誰かがこの順番で呼んでしまう日が来るのだろうと思うと、手のひらに汗が滲んだ。

「じゃあ、その窓口には、三つ全部の中身——水の量とか肥料の配合とか——も書いちゃえばいいんですか」

手入れ ── 三つの前に、窓口をひとつ

庭師さんがキーボードに手を伸ばし、新しい構造体を書き始めた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
pub struct FarmFacade {
    pub soil: SoilDiagnostics,
    pub irrigation: IrrigationControl,
    pub fertilization: FertilizationPlan,
}

impl FarmFacade {
    pub fn run_daily_care(&mut self) {
        match self.soil.diagnose() {
            SoilStatus::Saturated => {
                // 過湿時は、ここで止まる(早期リターン)
                return;
            }
            SoilStatus::Dry | SoilStatus::Normal => {}
        }
        self.irrigation.irrigate();
        self.fertilization.apply();
    }
}

SoilDiagnosticsIrrigationControlFertilizationPlanも、中身は一文字も変えていない。新しく増えたのはFarmFacadeだけだ。図にしてもらうと、呼び出しの道筋は一本にまとまっていた。

新人スタッフはrun_daily_careだけを呼び出し、run_daily_careが土壌診断・灌漑・施肥の順に一本道で呼び出す構造図

ヒバリさんが画面を指しながら説明を続ける。

run_daily_careがやってるのは、診断結果で分岐して、順番通りに呼ぶ、それだけです。水の量とか肥料の配合の計算は、ぜんぶ元のままSoilDiagnosticsIrrigationControlの中に残ってます」

さっきの私の問いに答えるように、庭師さんが小さく首を振った。ヒバリさんが引き取る。

「もし水の量の計算式まで、この窓口の中に書いていたら——計算式が変わるたびに、この窓口自体も直さなきゃいけなくなります。順番を守る、それだけがこの窓口の仕事なんです」

たしかにそうだ、と思う。うちの灌漑の水量は、畑の区画ごとに土質が違うから、これからも細かく調整が入るはずだ。もしその調整のたびに、順番まで管理しているこの窓口を触らなければならないとしたら——本来なら灌漑の担当者だけで完結する変更に、順番を扱う部分まで巻き込まれることになる。窓口が全部を抱え込んだら、今度はその窓口自体が、抱え込んだ分だけ重くなる。誰か一人が全部の相談ごとを抱え込む窓口みたいなものだと思うと、想像がつく。

「じゃあ、この窓口を通さずに、土壌診断だけ直接呼ぶ、みたいなことは、もうできないんですか」

台風が来たときは灌漑だけ手動で止めたい、という場面が頭をよぎって聞いてみる。庭師さんが画面のsoilのところを指で示した。

1
let status = farm.soil.diagnose();

「窓口は"いつもの通り道"なだけで、通行止めにはしていません。台風のときとか、灌漑だけ手動で止めたいときとかは、直接どうぞ」

ヒバリさんが補足する。「もしサブシステムの方から窓口を呼び返す作りにしてしまうと、順番を決めているのが窓口なのかサブシステムなのか、分からなくなってしまうんです。だから、窓口からサブシステムへの一方通行だけは崩しません。でも、皆さんの方から直接触れることは、ずっとできます。順番を強制するのと、直接触らせないのは、別の話なんです」

FarmFacadeの三つのフィールドは、pubのまま公開されている。窓口を通す一本道と、必要なときに直接触れる抜け道が、両方とも残っているらしい。

「じゃあ、新人が間違えて直接呼んじゃう心配は、まだ残ってるってことですか」

思わず聞き返すと、庭師さんが小さく頷いた。ヒバリさんが引き取る。「窓口は、正しい順番への近道であって、鍵のかかった壁じゃありません。新人研修ではrun_daily_careだけを教えて、直接触るのは、皆さんのようにサブシステムの中身を分かっている人だけ、というふうに運用で線を引くことになると思います」

図にしてもらって、ようやく腑に落ちた。窓口へ向かう太い道と、サブシステムへ直接抜ける細い道が、同じ地図の上に両方とも描かれている。片方を選んだらもう片方が消えるわけではないらしい。

利用者からrun_daily_careへ向かう太い実線の通り道と、土壌診断へ直接アクセスできる破線の抜け道が併存する構造図

将来、四つ目の管理項目——たとえば病害虫の監視——が増えたとしても、この窓口の形を変えずに済むということだろうと想像がついた。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#[test]
fn run_daily_care_skips_irrigation_and_fertilization_when_saturated() {
    let mut farm = FarmFacade {
        soil: SoilDiagnostics { moisture_level: 90 },
        irrigation: IrrigationControl { flow_rate: 0 },
        fertilization: FertilizationPlan { nutrient_mix: 0 },
    };

    farm.run_daily_care();

    assert_eq!(
        farm.irrigation.flow_rate, 0,
        "過湿時は早期リターンで灌漑がスキップされ、初期値のままのはず"
    );
    assert_eq!(
        farm.fertilization.nutrient_mix, 0,
        "過湿時は早期リターンで施肥がスキップされ、初期値のままのはず"
    );
}

土壌が過湿のときは、灌漑も施肥も一切実行されない。誰が呼んでも、run_daily_care一つを呼ぶ限り、この順番と分岐からは外れようがない。過湿でない場合もついでに確かめてみる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#[test]
fn run_daily_care_irrigates_and_fertilizes_when_soil_is_normal() {
    let mut farm = FarmFacade {
        soil: SoilDiagnostics { moisture_level: 50 },
        irrigation: IrrigationControl { flow_rate: 0 },
        fertilization: FertilizationPlan { nutrient_mix: 0 },
    };

    farm.run_daily_care();

    assert_eq!(farm.irrigation.flow_rate, 50);
    assert_eq!(farm.fertilization.nutrient_mix, 30);
}

正常な土壌のときは、灌漑も施肥もきちんと実行される。念のため、呼ばれる順番そのものも確かめておきたい。診断の後に灌漑、灌漑の後に施肥——この並びが本当に守られているかどうかは、flow_ratenutrient_mixの最終的な値を見るだけでは分からない。呼ばれた順番だけを記録する、専用の記録用の型を用意してテストした。書き換え可能な記録箱(RefCell)を、複数の場所から共有できるようにした(Rc)だけの、確認専用の道具だという。

 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
struct OrderTrackingSubsystem {
    log: std::rc::Rc<std::cell::RefCell<Vec<&'static str>>>,
    saturated: bool,
}

impl OrderTrackingSubsystem {
    fn diagnose(&self) -> bool {
        self.log.borrow_mut().push("diagnose");
        self.saturated
    }
    fn irrigate(&self) {
        self.log.borrow_mut().push("irrigate");
    }
    fn apply(&self) {
        self.log.borrow_mut().push("apply");
    }
    // run_daily_careと同一の制御フロー
    fn run_daily_care(&self) {
        if self.diagnose() {
            return;
        }
        self.irrigate();
        self.apply();
    }
}

run_daily_careと同じ制御フローを持つこの記録用の型を動かすと、ログには["diagnose", "irrigate", "apply"]という順番だけが残った。この記録用の型は本編のコードには登場しない、確認のためだけの道具だ。

「新人にも、run_daily_careだけ教えればいいってことですね。順番自体を覚える必要が、もう無い」

そう口にした途端、事務所を出てからずっと硬かった胸のあたりが、ようやく緩んだ気がした。

見送り ── 枝に触れる指先

コードを閉じるところまで見届けて、私は最後に確かめた。

「これで、経営が変わっても大丈夫そうです」

私がそう言うと、庭師さんは、それには答えず、ただ小さく頷いた。

見送りに出た庭師さんと連れ立って、入口の一角を通り過ぎる。傾き始めた秋の陽が、あの古い木の梢のあたりだけを金色に染めていた。庭師さんの手が伸びたのは、根元ではなく、思いのほか高い位置の枝だった。指先が幹の樹皮に一瞬だけ触れて、離れる。それだけの所作だったが、根元にしゃがみ込む姿しか想像していなかった私には、少し意外だった。触れる場所は、いつも同じとは限らないらしい。

タクシーを呼び直す間、私はもう一度あの枝を振り返った。収穫期でまだ慌ただしい農園に戻れば、また別の慌ただしさが待っている。それでも、新人スタッフに渡す言葉は、今朝よりずっと短くて済みそうだった。「土壌診断・灌漑・施肥は、このrun_daily_careを呼ぶだけでいい」——研修資料に書き足す一行を、車内で先に頭の中に組み立てておく。先代の頭の中にしかなかったものが、ようやく誰でも読める場所に移った。そう思うと、経営を引き継いでから初めて、自分の判断だけで前に進めた気がした。


手入れ記録

こんな症状が出たら入れるべき手入れ(パターン)まだ様子見でいい
複数の複雑なサブシステムを、利用者が直接それぞれ呼び出しており、正しい呼び出し順序を知らないと壊れる✓ Facade(順序制御だけを一つの窓口に閉じ込める)
サブシステムは一つしかない、あるいは呼び出し順序に依存関係がない✓ 現状のまま様子見

手入れの手順

  1. 利用者が直接呼んでいる複数のサブシステムから、守られるべき「呼び出し順序」を洗い出す(今回は土壌診断→灌漑制御→施肥計画)
  2. サブシステムをフィールドとして保持する窓口(Facade)用の構造体を新しく用意する
  3. 窓口のメソッドには、順序制御・分岐・早期リターンだけを書く。数値計算や業務ルールの判定は、元のサブシステム側に残したままにする
  4. サブシステムの型自体は公開したままにしておき、窓口を経由しない直接アクセスの余地も残す
  5. 窓口一つを呼ぶだけで正しい順序が守られることと、直接アクセスが壊れていないことの両方をテストで確かめる

庭師の一言

畝はいくつあっても、通う道は一つ。

見習いの手控え

今日は、先に説明するより先に聞いてみました。ご自分の言葉で気づいてもらえて良かったです。順番を守る道を一本作っておけば、通る人はその道の理由まで覚えなくていいんですね。

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