Featured image of post コードガーデナー【Factory Method】新しい品種を迎えるたび、同じ分岐にまた手を入れる〜生まれ方は、品種自身に持たせる〜

コードガーデナー【Factory Method】新しい品種を迎えるたび、同じ分岐にまた手を入れる〜生まれ方は、品種自身に持たせる〜

種苗店で働く私が、新品種バジルの入荷登録でピーマンのラベルを壊してしまう。生成ロジックを一つのmatch関数に集約していたせいだと知り、庭師が見せてくれたRustのFactory Methodで、生まれ方そのものを品種ごとに持たせる手入れ。

3月末、朝はまだ肌寒いが、日差しにわずかに春の気配がある。バス停から降り、いつもの通勤路——店の裏道——を急ぎ足で歩く。今日は夏野菜苗の入荷トラックが早めに来る予定で、頭の中は搬入の段取りでいっぱいだった。ふと、去年もちょうど同じ時期に、同じ道を、同じくらい急いで歩いていた気がする。季節が一周した、という実感が一瞬よぎるが、すぐにトラックの到着時刻のことに意識が戻る。

私は26歳、地元の種苗店で働き始めて4年目になる。数店舗を構える中堅チェーンの1店舗で、入荷担当として毎シーズンの品種構成を管理している。

訪れ ── バジルを足したら、ピーマンが壊れた

昨日のことを思い出す。今シーズン初めて仕入れることになったバジルの苗——入荷登録ツールに新しいアームを書き足した。近くにあったピーマンのアームをコピペして書き換えれば早いと思い、そうした。

今朝、出勤前にスマートフォンに店長からのメッセージが届いていた。

「ピーマンの棚のラベル、水やり間隔がおかしいんだけど確認してもらえる?」

血の気が引く。バジルを追加したときに、何かを壊した。

通勤路の途中、小さな工房の前を通りかかる。入口の脇に古い木が一本あるのを、今日は視界の端で捉えただけで通り過ぎようとした——が、ふと足が止まる。前に店の常連客から聞いたことがある。「コードの相談に乗ってくれる庭師さんがいるらしい」。今日はもう店に行っても、原因が分からないまま搬入時刻を迎えることになる。少しだけ寄り道することにした。

戸を叩くと、庭師が黙って顔を出す。

「すみません、急いでるんですけど……」と前置きしつつ、スマートフォンでコードの画面を見せる。「新しい品種を追加したら、別の品種のラベル表示が壊れたんです」。

庭師の後ろから、見習いらしい若い女性——ヒバリさんというらしい——が顔を出し、「まあ、座ってください」と作業台を示す。私は腰を下ろしながら、心の中で搬入時刻を数えていた。

私が普段使っている入荷登録ツールは、こんな形をしている。

 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
struct SeedlingRecord {
    name: String,
    price_yen: u32,
    watering_interval_days: u32,
    care_note: String, // 品種固有の世話の注意点を、自由記述の文字列一本にまとめている
}

fn create_seedling(variety: &str) -> SeedlingRecord {
    match variety {
        "トマト" => SeedlingRecord {
            name: "トマト".to_string(),
            price_yen: 300,
            watering_interval_days: 1,
            care_note: "支柱を立てて誘引する".to_string(),
        },
        "キュウリ" => SeedlingRecord {
            name: "キュウリ".to_string(),
            price_yen: 280,
            watering_interval_days: 1,
            care_note: "ネットを張って這わせる".to_string(),
        },
        "ナス" => SeedlingRecord {
            name: "ナス".to_string(),
            price_yen: 320,
            watering_interval_days: 2,
            care_note: "支柱を立てて誘引する".to_string(),
        },
        "ピーマン" => SeedlingRecord {
            name: "ピーマン".to_string(),
            price_yen: 280,
            watering_interval_days: 2,
            care_note: "支柱なしでも地這いで育てられる".to_string(),
        },
        _ => SeedlingRecord {
            name: variety.to_string(),
            price_yen: 0,
            watering_interval_days: 0,
            care_note: "未登録の品種です。店長に確認してください。".to_string(),
        },
    }
}

品種名を渡すと、価格・水やり間隔・世話の注意点をまとめて返してくれる。今回、このmatchにバジルのアームを足す際、ピーマンのアームをコピペして書き換えた——つもりだったが、care_noteの書き換えを一部忘れたまま保存していた。ピーマンのラベルに、バジルの水やり文言が混ざっていた。

症状観察 ── 首が、わずかに傾く

庭師は、差し出された画面——create_seedling関数——に目を落とす。何も言わない。トマト・キュウリ・ナス・ピーマン・バジルと並んだ5つのmatchアームを、上から下へゆっくりとなぞっていく。

私は、その沈黙に落ち着かなくなって「バジルのアーム、ピーマンのをコピペしたのがまずかったですか」と聞いてしまう。庭師は答えない。ただ、わずかに首が傾いた——それが何のサインなのか、私にはまだ分からない。

庭師は、care_noteという一つのフィールドに、視線をしばらく留めた。トマトは「支柱を立てて誘引する」、キュウリは「ネットを張って這わせる」、ピーマンは「支柱なしでも地這いで育てられる」——形の違う世話の情報が、同じ一つの文字列フィールドに、無理やり押し込まれている。庭師の視線は、その押し込まれ方を確かめるように動いていた。

庭師の視線が留まっていた場所を、私も画面の上でなぞってみる。トマトも、キュウリも、ナスも、ピーマンも——形の違う世話の情報が、care_noteという同じ一つの箱に詰め込まれている。バジルのアームは、その隣に付け足された。

一枚岩のcreate_seedling関数から5品種がぶら下がり、同じcare_note文字列に世話の情報を詰め込んでいる図。ピーマンとバジルの間だけ紐がもつれてコピペ事故を示す

見立てと翻訳 ── 「品種は、増え続ける」

しばらくして、庭師がぽつりと言う。

「品種は、増え続ける」

ヒバリさんが、私の方を向いて言い換える。「つまり、品種をenumにすればいいってことですよね」。私は「あ、enumなら書いたことあります」と少し前のめりになる。庭師が、短く一言だけ添えた。

「それだと、また同じことが起きる」

ヒバリさんが、はっとした顔をした——ように見えた。

「すみません、今のは少し違いました」とヒバリさんが言い直す。「enumは、あらかじめ決まった選択肢の集合——閉じた集合——を表すのに向いているんです。いくつかの選択肢から一つを選ぶための型、と考えてください。でも、種苗店の品種って、毎シーズン増えますよね。それは、閉じてない集合なんです」。

私は頷く。「確かに、来年もまた新しい品種が入るかもしれません」。

「そうなんです」とヒバリさんが続ける。「enumに新しい品種を足すと、そのenumを使っているmatchが、Rustのコンパイラに『網羅してない』って怒られます。これを網羅性チェックと呼びます——matchはあり得るすべてのパターンを尽くしていないとコンパイルが通らない、という仕組みです。新しい選択肢を足すたび、その選択肢を使っているmatchを全部洗い出して直す羽目になります」。

「今回、私が壊したのって……」と私は自分のコードを見返す。「品種を1箇所のmatchに集約してたから、というのもありますよね」。

ヒバリさんが頷く。「はい。それに加えて、品種ごとの世話の情報を、全部同じ一つの文字列フィールドに詰め込んでたのも大きいです。トマトの支柱の話とバジルの摘心の話、本当は全然違う"形"の情報なのに、同じcare_noteという箱に入れてます」。

私は経緯を話す。「最初はトマトとナスの2品種だけで、正直これで十分でした。キュウリとピーマンが増えたときも、care_noteに一言足すだけで済んでたので、特に困ってなかったんです」。

「そうですよね、最初はそれで足りてたんです、悪い判断じゃないです」とヒバリさんが言う。庭師も、小さく頷いた。

「じゃあ、どうすれば、品種を足しても他の品種を壊さずに済むんですか」と私は結論を急いだ。

庭師が短く答える。

「生まれ方を、品種自身に持たせる」

私は「え」と聞き返す。ヒバリさんが続ける。「1つの関数に全品種の生成ロジックを集約するんじゃなくて、品種ごとに専用の"生まれ方"を持たせるんです。それがFactory Methodです」。

手入れ ── 品種ごとに、専用の生まれ方を書く

庭師がノートパソコンに手を伸ばす。私が場所を譲ると、庭師はまずSeedlingというトレイトを書いた。ヒバリさんが実況する。

「トレイトは、複数の型に共通の振る舞いを約束させる仕組みです。Seedlingは『苗であること』を約束するトレイト——名前・価格・水やり間隔・世話ラベルを持つ、という約束だけをここに書きます」。

1
2
3
4
5
6
trait Seedling {
    fn name(&self) -> String;
    fn price_yen(&self) -> u32;
    fn watering_interval_days(&self) -> u32;
    fn care_label(&self) -> String; // 品種固有のフィールドから組み立てる
}

続けて庭師は、もう一つのトレイトを書いた。

1
2
3
trait SeedlingFactory {
    fn create(&self) -> Box<dyn Seedling>;
}

「こっちは『苗を生み出す』ことを約束するトレイトです」とヒバリさんが言う。「Seedlingが"何であるか"を決めるのに対して、SeedlingFactoryは"どう生まれるか"だけを決めます。役割が違うんです。生まれる側を"Product役"、生み出す側を"Creator役"と呼ぶこともあります」。

庭師がTomatoSeedlingという構造体を書き始める。フィールドはneeds_support: boolひとつだけ。

 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
struct TomatoSeedling {
    needs_support: bool,
}
impl Seedling for TomatoSeedling {
    fn name(&self) -> String {
        "トマト".to_string()
    }
    fn price_yen(&self) -> u32 {
        300
    }
    fn watering_interval_days(&self) -> u32 {
        1
    }
    fn care_label(&self) -> String {
        if self.needs_support {
            "支柱を立てて誘引する".to_string()
        } else {
            "支柱なしで育てられる".to_string()
        }
    }
}

struct TomatoFactory;
impl SeedlingFactory for TomatoFactory {
    fn create(&self) -> Box<dyn Seedling> {
        Box::new(TomatoSeedling { needs_support: true })
    }
}

「トマトだけの都合ですか」と私が聞くと、ヒバリさんが頷く。「はい。トマトには支柱の要否、キュウリにはネットの要否——品種ごとに、本当に必要な情報だけを持たせます。care_noteみたいな汎用の文字列箱には、もう戻しません」。

同じ形で、キュウリ・ナス・ピーマンにもそれぞれ専用の構造体とFactoryができていく。キュウリならneeds_trellis: bool、ナス・ピーマンならneeds_support: bool——同じ「支柱の要否」でも、ナスとピーマンでは実際の値(truefalse)が違う。

Box<dyn Trait>という書き方が出てきたのは、これが初めてではない。前の回(りんご農家の剪定計画の話)で、複数の異なる具象型を同じコレクションに入れたいときに使うと聞いた——今回は、create()メソッドが「どのFactoryを呼んでも、Box<dyn Seedling>という同じ型で受け取れる」ようにするために使われている。

品種の数だけProduct型とFactory型が並んだところで、私は聞いた。「バジルを足すときは、どこに書けばいいんですか」。

ヒバリさんが答える。「BasilSeedlingBasilFactoryを、新しく書くだけです。トマトのファイルにもキュウリのファイルにも、一切触れません」。

 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
struct BasilSeedling {
    needs_pinching: bool, // 摘心が必要か
}
impl Seedling for BasilSeedling {
    fn name(&self) -> String {
        "バジル".to_string()
    }
    fn price_yen(&self) -> u32 {
        250
    }
    fn watering_interval_days(&self) -> u32 {
        1
    }
    fn care_label(&self) -> String {
        if self.needs_pinching {
            "芽先を摘心して枝分かれさせる".to_string()
        } else {
            "そのまま育てられる".to_string()
        }
    }
}

struct BasilFactory;
impl SeedlingFactory for BasilFactory {
    fn create(&self) -> Box<dyn Seedling> {
        Box::new(BasilSeedling { needs_pinching: true })
    }
}

私は自分のBeforeコードを思い浮かべた。「……ピーマンのアームの近くを通らなくて済む、ってことですね」。

「呼び出す側は、どう変わるんですか」と私が聞くと、庭師が別の関数を指す。

1
2
3
fn receive_shipment(factory: &dyn SeedlingFactory) -> Box<dyn Seedling> {
    factory.create()
}

&dyn SeedlingFactoryを受け取って、create()を呼ぶだけ」と庭師が短く言う。ヒバリさんが補足する。「新しい品種が来たら、receive_shipment(&BasilFactory)という呼び出し文を1行足すだけです。既存のreceive_shipment(&TomatoFactory)みたいな行は、一切書き換えません」。

私は少し引っかかって聞いた。「でも、入荷伝票には品種名が文字列で書いてあるんですけど、その文字列からどのFactoryを使うか、結局どこかで対応させる必要ありませんか」。

庭師が、初めてわずかに間を置く。

「今は、要らない」

ヒバリさんが続ける。「入荷担当——あなた自身が、今日届いた品種が何かを、もう分かってますよね。だから、コードの中で直接&BasilFactoryって指定できます。もし将来、伝票の文字列から自動でFactoryを選びたくなったら、りんご農家の剪定計画のときみたいに、品種名をキーにしたレジストリを作ることもできます。今回はそこまで必要ないので、省いてあります」。

私は「なるほど、今の業務だと、そこまでは要らないんですね」と納得した。

「これ、前に知り合いから聞いた、りんご農家の剪定計画の話と似てませんか」とふと聞いてみる。matchが増えすぎて困る、という出だしが同じ気がしたからだ。

ヒバリさんが少し考えてから答える。「似てるところもありますけど、違うところもあります。その話のレジストリは、もう出来上がってる実装を名前で棚から取り出すだけでした。今回は違います。create()という"生まれる瞬間"そのものを、品種ごとのFactoryに分けてるんです。だから、どの品種でもVec<String>という同じ形の結果が返ってきて、中身——判定ロジック——だけが違う、という作りにはなりません。TomatoSeedlingBasilSeedlingは、返ってくる型そのものが違うんです。care_label()の文字列だけを見ても、その裏にneeds_supportがあるのかneeds_pinchingがあるのか、外からは分かりません。品種ごとに、本当に違う"形"を持ってるんです」。

私は自分のBeforeコードのcare_noteを思い出した。「あの汎用の文字列フィールドに全部押し込んでたのが、まさにその"形の違い"を潰してたんですね」。

品種の数だけ並んだProduct型とFactory型を、あらためて見返す。トマトも、ピーマンも、そしてバジルも——それぞれが自分の"生まれ方"を、自分の札に持っている。隣の品種の札には、指一本触れていない。

Seedling(Product役)とSeedlingFactory(Creator役)を頂点に、トマト・キュウリ・ナス・ピーマン・バジルそれぞれが独立したProduct型とFactory型のペアを持つ図。バジルのペアだけ新しい木目で新規追加を示す

見送り ── 「同じ種から、同じ苗しか生まれると思うか」

私は、書き直したコードを自分のノートパソコンへ書き写しながら、庭師に聞いた。「これで、バジルを足してもピーマンは壊れませんよね」。

庭師は頷く。「ああ」。

ヒバリさんが「さっきは、私の説明が浅かったですね、すみません」と少し照れたように言う。私は「いえ、直してもらえたので分かりやすかったです」と答えた。

戸口まで見送りに出た庭師が、最後に一言だけ添える。

「同じ種から、同じ苗しか生まれると思うか」

私はその問いを、頭の中でもう一度なぞった。

工房を出て、通勤路の続きを歩きながら、入口の脇のあの古い木の前を通りかかる。今日は、思わず足を止めた。

「あの、この木っていつからあるんですか」

庭師の方を振り返って聞いてみる。庭師は、少し間を置いてから、短く答えた。

「さあ」

それ以上は何も続かなかった。はぐらかされたのか、本当に知らないのか、判断がつかないまま、私は会釈をして歩き出した。

バス通りへ戻りながら、私は独りごちる。「生まれ方を、品種自身に持たせる、か」。搬入トラックが到着するまで、あと少し。今シーズン、もし他にも新しい品種を仕入れることになったら、今度は迷わず専用の作り方を書き足そう、と思った。そして、さっきの「さあ」という一言が、なぜか頭に残り続けていた。


手入れ記録

こんな症状が出たら入れるべき手入れ(パターン)まだ様子見でいい
種類(品種)が増えるたびに、1つの生成関数へ手を入れて既存の分岐を壊しかける✓ Factory Method(Product・Creatorをtrait化)
品種ごとに本来違う"形"のデータを、無理に同じ汎用構造体へ押し込んでいる✓ Factory Method(品種ごとに専用のProduct型を持たせる)
種類が2〜3個で、今後も増える予定が無い✓ そのままのmatchで様子見

手入れの手順

  1. 生成関数の中で、品種ごとに枝分かれしている箇所と、無理に共通化されているフィールドを洗い出す
  2. 品種ごとに専用の型(Product)を作り、共通のふるまい(トレイト)としてまとめる
  3. 品種ごとの生成ロジックを、対応するFactory型のメソッドへそのまま移す(ロジックは変えない)
  4. 呼び出し側は、具象Factory型を直接指定して呼ぶ形に置き換える
  5. 新しい品種を追加するときは、新しいProduct型とFactory型を書き、呼び出し文を1行足すだけにする。既存の型・呼び出し文には触れない

庭師の一言

同じ種から、同じ苗しか生まれると思うか。

見習いの手控え

師匠の言葉、要は「品種ごとに、生まれ方そのものを持たせる」ってことです。今日は、私の説明が最初ちょっと浅かったですね——enumで十分だと思ったんですが、品種がこれからも増え続けるなら、話は変わってくるんです。

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