Featured image of post コードカーペンター【Sprout Class】「今の蔵には、置けないんですね」〜既存に入らないなら、新しい部材を別に起こす〜

コードカーペンター【Sprout Class】「今の蔵には、置けないんですね」〜既存に入らないなら、新しい部材を別に起こす〜

蔵に新区分「混載」を設けたい依頼が来る回。既存の在庫クラスは帳簿ファイルを開くコンストラクタを持ち、テストの中で組み立てられません。芽生やしクラス(Sprout Class)で判定ロジックを別クラスに起こし、既存へは委譲だけを足します。

第1幕: 検分 ── 蔵の前、まだ何も決まっていない

蔵の前は、表とも台所とも違う空気だった。土壁のひんやりした感触が、少し離れていても分かる。分厚い引き戸には錠前が下がっていて、開け閉てのたびに、蝶番がわずかに軋む音がした。朝のうちはまだ日が当たらず、土の匂いだけが濃く残っている場所だった。

通り庭を抜けて蔵まで来る道は、三十年のあいだに数え切れないほど歩いてきた。荷を担いだ人がすれ違うだけの、あの細い土間の先に、この蔵はある。以前、通り庭の床下でどこまで響くかを見立てたときのことを、ふと思い出した。あのときは荷の流れる経路の話だったが、今日はその先の、荷が最後に落ち着く場所の話だった。

クロベさんとナギさんは、もうそこにいた。ゆうべのうちに、台所の一件(それが芽生やしメソッドの話だったと知ったのは、あとになってからだ)の続きを、蔵の前で片付けていたらしい。私が近づくと、二人ともこちらへ顔を向けた。今日は、私のほうから用件を持ってきていた。

「店主が、蔵に新しい区分を設けたいと仰っていて」

私は、聞いてきたままを口にした。近ごろ、荷の届き方が少し変わってきている。生鮮のものと日用品のものが、同じ荷にまとめて届くことが増えたのだという。今の帳面には、品目ごとに「生鮮」か「日用品」かの区分しかない。だから、そういう荷をどう扱えばいいのか、帳面を見ただけでは分からない。店主は、それを「混載」という新しい区分として、蔵の帳面に加えたいと言っていた。

「それだけなら、簡単そうですね」

ナギさんが、聞き終わるより先に言った。

「前と同じように、Kura::Ledgerに新しいメソッドを足せばいいですよね」

Kura::Ledgerというのは、蔵の帳面を扱っている今のクラスの名前だった。品物の名前を渡すと、帳面に記された区分を返してくれる。ナギさんの頭の中では、もう昨日の続きが動いているようだった。新しい処理を、そのクラスの中に新しく書き足すだけ――昨日、台所で覚えたばかりのやり方だ。

クロベさんは、すぐには頷かなかった。ナギさんの顔を見て、静かに問い返した。

「そのメソッドを確かめるには、何が要りますか」

ナギさんが、一瞬止まった。答えを急ぐでもなく、かといって黙り込むでもなく、自分の中で何かを組み立て直しているような間だった。

「……テスト、ですよね。Kura::Ledgerを作って、新しいメソッドを呼んで」

「作る、というのは」

クロベさんが、また問い返した。答えを教えるふうではなく、ナギさん自身に確かめさせるふうに、私には聞こえた。

そこで、いったん間が空いた。まだ、誰も答えを出していない。

第2幕: 手が入らない ── ナギ自身の気づき

ナギさんは、自分の言葉に自分で応えるように、画面に向かった。

「Kura::Ledgerを、作ってみます」

そう言って、蔵の帳面を扱うライブラリの隅に、短い一行を書いた。

1
my $ledger = Kura::Ledger->new;

引数を何も渡さない、ただ作るだけの一行だ。動かしてみると、すぐに画面が返してきた。

1
2
3
Use of uninitialized value $path in open at lib/Kura/Ledger.pm line 11.
Use of uninitialized value $path in concatenation (.) or string at lib/Kura/Ledger.pm line 11.
帳簿ファイルを開けません:  (No such file or directory) at lib/Kura/Ledger.pm line 11.

ナギさんは、しばらくその文字を見ていた。

「……あ。蔵の帳簿を開かないと、そもそも作れないんですね」

自分で言ってから、少し驚いたような顔をした。前回の台所の一件では、新しいメソッドを試すのに、道具箱以外は何も要らなかった。今度は違う。Kura::Ledgerを一つ作るだけで、実在する帳簿ファイルが要る。

クロベさんが、そこで言葉を継いだ。

「Kura::Ledgerを一つ作るだけで、本物の帳簿ファイルが要ります。新しいメソッドを確かめようとするたび、そのファイルを毎回用意しなければなりません」

「昨日は、そんなことをしなくても、新しいsubだけをすぐに試せました」

ナギさんが言った。昨日との違いを、自分の言葉でなぞっているようだった。

「昨日のDelivery::calc_feeは、それ自体は重さも距離も、ただの数字を受け取るだけの関数でした。今日のKura::Ledgerは違います。作るという行為そのものが、外にある本物のファイルを開きに行きます」

クロベさんは、そこで一度言葉を区切った。

「これは、読みにくいから直しにくい、という重さではありません。対象そのものが、そもそもテストの土俵に乗らない、という重さです。昨日使った芽生やしメソッドは、対象がテストの土俵に乗っていることを前提にしています。この壁の前では、その手順は成り立ちません」

「土俵に乗らない、というのは、動かないということですか」

ナギさんが訊いた。今のdieを見たばかりの言葉としては、無理もない問いだった。

「動かなくはありません。試すたびに、本物の帳簿ファイルを用意すれば、動きます。ですが、新しいメソッドを一つ試すたびに、本物と同じ形のファイルを毎回こしらえるのは、割に合いません。テストというのは、気軽に何度も動かせてこそ意味があります」

私は、それを聞きながら、今の蔵の戸を思い浮かべていた。毎朝、私が開け閉てしているあの重い戸だ。実在するものを、毎回きちんと開かなければ動かない――そういう重さには、心当たりがあった。

「今の蔵には、置けないんですね」

私は、確かめるように言った。今のKura::Ledgerの中に、新しい判定を置くという話ではない、ということが、ようやく飲み込めてきたところだった。

クロベさんは、私の言葉にゆっくりと頷いた。そして、静かに言い切った。

「置き場所が無いのなら、探すだけ無駄です」

それだけだった。次にどうするのかは、まだ言わない。ナギさんも、私も、その先を待つしかなかった。

第3幕: 手立て ── 母屋の外に、離れを起こす

クロベさんは、Kura::Ledgerのコンストラクタを開いて、上から順に目で追った。指でなぞる仕草はしなかった。文字を追う目だけが、ゆっくりと動いていた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
sub new {
    my ($class, %args) = @_;
    my $path = $args{ledger_file};

    open my $fh, '<', $path
        or die "帳簿ファイルを開けません: $path ($!)";

    my %categories;
    while (my $line = <$fh>) {
        chomp $line;
        next unless length $line;
        my ($item, $category) = split /\t/, $line;
        die "帳簿の書式が壊れています: $line"
            unless defined $item && defined $category;
        $categories{$item} = $category;
    }
    close $fh;

    return bless { categories => \%categories }, $class;
}

「これは、帳簿ファイルの書式が壊れていないかを、読み込みの都度検めています」

クロベさんが言った。

「だから、軽々しくは動かせない、ということですか」

私が訊くと、クロベさんは頷いた。

「これを手抜きだとは思わないでください。この検めがあるから、書式の壊れた帳簿を、そのまま黙って読み込んでしまうことがない。今日まで、それを支えてきました。ただ、支えているものが重いという事実は変わりません」

過去にどうしてこうなったのかを、クロベさんは言わなかった。今、何を支えているかだけを口にしていた。

「今日入れたいのは、混載かどうかの判定です。これは、帳簿ファイルの中身とは別の話です。品目ごとの区分さえ分かれば、判定そのものは成り立ちます」

クロベさんは、そこで正式な名前を口にした。

「対象のクラスが、テストの土俵に妥当な時間で乗らないとき――今日のように、作るだけで外の資源が要る場合などです――追加する処理を新しい独立したクラスとして別に起こし、既存のクラスからは、そのクラスを使うだけにする――これを、芽生やしクラス(Sprout Class)と言います」

「入らないものは、母屋の中には据えられません。外に、離れを起こす――それだけの話です」

「昨日の、芽生やしメソッドと似ていますね」

ナギさんが言った。

「似ています。名前も、考え方も。違うのは、置き場所です。昨日の根継ぎ――大工の呼び名で言えば芽生やしメソッドのことですが――は、母屋の中の、腐った所だけを切って新材を継ぐ。既存クラスの内側に、新しい処理を足します。今日のは、母屋の中に置き場所そのものが無い。だから、外に新しく起こすしかありません」

私は、二つの技法の違いを、野帳に書き分けた。中に足すか、外に起こすか。それだけの違いが、これほど大きな判断を分けるとは思わなかった。

野帳の文字だけでは足りない気がして、二つの図を並べて描いてみた。

構造対比図。左半分は前話の根継ぎ(芽生やしメソッド)—母屋Kura::Ledgerの箱に、新設subの小さな箱が直接繋がっている。右半分は今話の離れ(芽生やしクラス)—母屋Kura::Ledgerの箱から離れた位置に、独立したKura::CategoryJudgeの箱があり、classify_shipmentという矢印だけで繋がっている

内側に足す線と、外側に建てる線。同じ紙の上に並べると、違いは一目で分かった。

「まず、新しいクラスから起こします。名前はKura::CategoryJudge。実ファイルには一切触れず、区分の一覧だけを受け取ります」

 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 Kura::CategoryJudge;

use strict;
use warnings;

sub new {
    my ($class, %args) = @_;
    my $categories = $args{categories} || [];
    return bless { categories => $categories }, $class;
}

sub judge {
    my ($self) = @_;

    my %seen;
    for my $category (@{ $self->{categories} }) {
        next if $category eq '未分類';
        $seen{$category} = 1;
    }

    my @distinct = keys %seen;
    return '未分類'    if @distinct == 0;
    return $distinct[0] if @distinct == 1;
    return '混載';
}

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
use strict;
use warnings;
use Test::More;
use lib 'lib';
use Kura::CategoryJudge;

subtest '単一区分のみなら、その区分をそのまま返す' => sub {
    my $judge = Kura::CategoryJudge->new(categories => ['生鮮', '生鮮']);
    is($judge->judge, '生鮮', '同じ区分だけが並べば、その区分をそのまま返す');
};

subtest '複数の区分が混ざれば、混載と判定する' => sub {
    my $judge = Kura::CategoryJudge->new(categories => ['生鮮', '日用品']);
    is($judge->judge, '混載', '異なる区分が混ざれば混載と判定する');
};

subtest '未分類だけなら未分類のまま' => sub {
    my $judge = Kura::CategoryJudge->new(categories => ['未分類', '未分類']);
    is($judge->judge, '未分類', '未分類しか無ければ未分類のまま返す');
};

subtest '未分類が混ざっても、実在区分が1つなら混載にしない' => sub {
    my $judge = Kura::CategoryJudge->new(categories => ['生鮮', '未分類']);
    is($judge->judge, '生鮮', '未分類は区分の1つとして数えないので、実在区分が1つなら混載にならない');
};

done_testing;

動かしてみると、四つとも一度で通った。

1
2
3
4
5
ok 1 - 単一区分のみなら、その区分をそのまま返す
ok 2 - 複数の区分が混ざれば、混載と判定する
ok 3 - 未分類だけなら未分類のまま
ok 4 - 未分類が混ざっても、実在区分が1つなら混載にしない
1..4

「未分類が混ざっても、混載にしない、というのは」

私が訊いた。

「未分類は、区分が無いという意味です。区分の一つとして数えてしまうと、生鮮の品目に、区分の無い品目が一つ混ざっただけで混載になってしまいます。それは、今回店主が言っている混載とは違います」

言われてみれば、その通りだった。区分が無いことと、別の区分であることは、同じではない。

「次は、category_forの今の振る舞いを書き取っておきます。前と同じ要領です」

クロベさんが言った。ナギさんが、「ああ、あれですね」と、短く応えた。category_forそのものは、短い関数だった。

1
2
3
4
sub category_for {
    my ($self, $item) = @_;
    return $self->{categories}{$item} // '未分類';
}

「これは、渡された品目名を、さっき読み込んだ区分の一覧から引くだけです。帳簿に無い品目なら、未分類を返します」

クロベさんが言った。中身そのものは、確かに単純だった。重いのは、この関数ではなく、その前段にあるnewのほうだ。今回は、帳簿ファイルという実在のものを使う都合上、まずその中身を決めておく必要があった。

1
2
3
4
大根	生鮮
鰹節	生鮮
茶碗	日用品
醤油	日用品

このような、品目と区分をタブで区切った帳簿ファイル(フィクスチャ)を、テスト専用に一つ用意した。実際に蔵にある帳簿と同じ形式だが、テストのためだけに使う、決まった中身のものだ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
use strict;
use warnings;
use Test::More;
use lib 'lib';
use Kura::Ledger;

subtest '生鮮の品目' => sub {
    my $ledger = Kura::Ledger->new(ledger_file => 't/fixtures/kura_ledger.tsv');
    is($ledger->category_for('大根'), '生鮮', '帳簿にある生鮮の品目は生鮮を返す');
};

subtest '日用品の品目' => sub {
    my $ledger = Kura::Ledger->new(ledger_file => 't/fixtures/kura_ledger.tsv');
    is($ledger->category_for('茶碗'), '日用品', '帳簿にある日用品の品目は日用品を返す');
};

subtest '帳簿に無い品目は未分類' => sub {
    my $ledger = Kura::Ledger->new(ledger_file => 't/fixtures/kura_ledger.tsv');
    is($ledger->category_for('炭'), '未分類', '帳簿に記載の無い品目は未分類を返す');
};

done_testing;

三つとも通った。この時点で、Kura::Ledgerにはまだ、一行も手を入れていない。

「最後に、Kura::Ledgerへ、呼び出しを足します」

クロベさんは、まずファイルの先頭近く、use warnings;の下に一行だけ加えた。

1
use Kura::CategoryJudge;

そのうえで、newとcategory_forはそのままに、末尾に新しいメソッドを一つ書き加えた。

1
2
3
4
5
sub classify_shipment {
    my ($self, @items) = @_;
    my @categories = map { $self->category_for($_) } @items;
    return Kura::CategoryJudge->new(categories => \@categories)->judge;
}

「先頭のuseが一行と、末尾に新しいメソッドが一つ。それだけです」

「newも、category_forも、そのままですね」

私が確かめるように言うと、クロベさんは頷いた。

「変えていません。新しい責務を、Kura::Ledgerに増やしてもいません。品目を区分に変えて、Kura::CategoryJudgeに渡す――それだけの窓口を、一つ増やしただけです」

「昨日の根継ぎとは、また違うんですね」

ナギさんが言った。

「違います。昨日は、同じクラスの中に、新しいsubを一本足しました。今日は、判定そのものを、まったく別のクラスに任せています。Kura::Ledgerがしているのは、品目を区分に変えて、渡す、という橋渡しだけです」

私は、そこで一つ気になっていたことを口にした。

「これは、Kuraとは、また別のものなんですか」

蔵には、以前からKuraという名前のパッケージがあった。品物を確保するときに使う、窓口のようなものだ。中身は、確保した品物を覚えておく変数と、それを扱う手続きがあるだけで、今日のKura::Ledgerのような、作るという手順はなかったと記憶している。

「別のものです」

クロベさんが答えた。

「Kuraは、品物を蔵の中に確保する窓口です。Kura::Ledgerは、蔵の帳面をつける係です。同じ蔵という場所を扱いますが、役目が違います。窓口を、帳面つけの係に書き換えたわけではありません」

蔵には、品物を出し入れする窓口とは別に、帳面をつける係がある――というだけの話らしい。私は、それを自分の言葉に直して、野帳の隅に書き留めた。classify_shipmentをどの経路から呼ぶようにするかは、今日の話にはまだ出てこなかった。それはまた、別に決めることなのだろう。

野帳の最後に、今日起こしたものと、触らなかったものを描き分けておいた。

Kura::Ledgerクラスの委譲図。左の枠内、灰色破線のnew(ledger_file)とcategory_for(item)は触っていない要素、黒い実線のclassify_shipmentは今日足した窓口。右の枠内、Kura::CategoryJudgeのnewとjudge()はともに新規。classify_shipmentから区分の配列を渡してKura::CategoryJudgeへ矢印が伸びている

灰色の線が触らなかった所、黒い線が今日建てた所。見比べれば、どこに何を足したのか迷わなかった。

第4幕: 検め ── 新しく起こす所は、誰が決めるか

三つの検めを、順番にもう一度動かした。

まず、既存のcategory_forの書き取りを、そのままもう一度動かした。三つとも、変わらず通った。classify_shipmentを足す前と後で、既存の振る舞いは何も変わっていない。

次に、新しく足したclassify_shipmentそのものを確かめた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
use strict;
use warnings;
use Test::More;
use lib 'lib';
use Kura::Ledger;

subtest '生鮮どうしの荷は混載にならない' => sub {
    my $ledger = Kura::Ledger->new(ledger_file => 't/fixtures/kura_ledger.tsv');
    is($ledger->classify_shipment('大根', '鰹節'), '生鮮', '生鮮の品目だけの荷は生鮮のまま、混載にならない');
};

subtest '生鮮と日用品が同じ荷に入れば混載' => sub {
    my $ledger = Kura::Ledger->new(ledger_file => 't/fixtures/kura_ledger.tsv');
    is($ledger->classify_shipment('大根', '茶碗'), '混載', '異なる区分の品目が同じ荷に混ざれば混載と判定する');
};

subtest '品目が一件も無い荷は未分類のまま' => sub {
    my $ledger = Kura::Ledger->new(ledger_file => 't/fixtures/kura_ledger.tsv');
    is($ledger->classify_shipment, '未分類', '品目を1件も渡さなければ、区分の集合が空になり未分類を返す');
};

done_testing;

三つとも通った。

「三つだけで、いいんですか」

私は、少し不安になって訊いた。

「category_forが、それぞれの品目を正しい区分に変えることは、さっきの書き取りで確かめてあります。Kura::CategoryJudgeが、区分の組み合わせから正しく判定を出すことも、単体のテストで確かめてあります。classify_shipmentがしているのは、品目を区分に変えて渡す、という橋渡しだけです。橋渡しがきちんと動いていることと、荷が空だったときに未分類のまま壊れずに済むこと、この二つさえ示せれば、判定のあらゆる組み合わせをここでもう一度確かめ直す必要はありません」

言われてみれば、確かに、それぞれ別のところで確かめたものを、もう一度重ねて確かめる必要はない。橋渡しの部分と、その境目にある空の荷だけを確かめれば足りるということだった。

「保証するのは、ここまでです」

クロベさんが、区切るように言った。

「今のcategory_forの振る舞いは変わっていません。混載かどうかの判定も、確かめてあります。保証しないのは、区分の判定を、もっと細かくするという話です。今日のKura::CategoryJudgeが背負っているのは、区分が一つか、複数かを見るだけの、単純な判定です。もし今後、混載の中でも生鮮の割合によって扱いを変える、という話が来れば、この単純な判定では足りなくなります」

「今日は、Kura::Ledgerの重さそのものには触れていないんですね」

私が言うと、クロベさんは頷いた。

「触れていません。帳簿ファイルを開く、あの重さ自体を軽くする話は、今日の範囲ではありません。今日は、重さに手を付けず、新しい判定だけを外に置く話でした」

クロベさんは、そこで少し声を落として、続けた。

「一つだけ、正直に言っておきます。新しい部材を起こすたび、今まで一つの中にあったはずの仕事が、あちこちに散らばります。それが正しいこともあれば、ただ今はそうするしかない、というだけのこともあります。今日のこれも、そのどちらかは、しばらく経ってみないと分かりません」

技法の効き目だけを語られると思っていたので、その一言は少し意外だった。だが、嘘や誇張のない言い方だとも思えた。

「じゃあ、新しいクラスは、無闇に増やさないほうがいいんですか」

ナギさんが訊いた。

「無闇に、とは言いません。今日みたいに、母屋の中に置き場所が無いと分かったときは、迷わず外に起こせばいい。ただ、それを毎回の癖のようにはしないでください。外に起こした数が増えるほど、どこに何があるかを、誰かが覚えていなければならなくなります」

蔵の前には、いつのまにか日が差し込み始めていた。土壁の匂いも、朝のうちより少し和らいで感じられる。ナギさんは道具箱――今日は画面だけだったが――を片付け、クロベさんは腕を組んで、しばらくKura::Ledgerの画面を見ていた。

私は帳場に戻ると、いつもより急いで野帳を開いた。今日は、答えより先に、自分の問いのほうを書き付けておきたかった。

新しく起こす所は、誰が決めるんですか。

そう書いてから、クロベさんの言葉を思い出した。置き場所そのものは、今日みたいに、外に建てるしかない場合が分かれば、あとは好きに決めていい。

新しく起こすものの置き場所を自分で決めてよい。

そう書いてから、少し手が止まった。今日クロベさんが示してくれた「入るか入らないかの見極め」――コンストラクタが実ファイルに触れるかどうか――は、まだ自分の言葉にしきれていないと感じた。それは、次に自分で見極めるときの課題として、心のどこかに置いておくことにした。

窓の外で、蔵の戸が軋む音がした。誰かが、今日もあの重い戸を開け閉てしているのだろう。

クロベさんは、まだ画面のほうを向いたまま、独り言のように言った。

「今日は、外に一つ起こしただけです。多く起こしすぎれば、それも見通しを悪くします」

それだけだった。褒めるでもなく、念を押すでもない一言だった。私は野帳を閉じ、表紙の上に手を重ねた。中に足すか、外に起こすか――今日初めて自分で分けたその線が、この一冊の中で、確かに一つ増えていた。


普請控

  • 見立て: 店主から、蔵に生鮮と日用品が混ざって届く荷(混載)のための新しい品目区分を設けたいという要望が来た。既存の蔵の帳簿クラス(Kura::Ledger)は実際の帳簿ファイルを開いて書式を検めるコンストラクタを持ち、テストの中で気軽に組み立てられなかった
  • 手を入れた所: 芽生やしクラス(Sprout Class)を使い、新しい区分の判定ロジックを、実ファイルに一切触れない独立したクラスKura::CategoryJudgeとして起こし、単独でテストした。Kura::Ledgerへは、委譲メソッドclassify_shipmentを一つ、useの一行と合わせて足した
  • 触っていない所: Kura::Ledgerのコンストラクタそのもの(帳簿ファイルを開いて書式を検める重さ)。category_forの中身。区分の判定をさらに細かくする(生鮮の割合で扱いを変える等)かどうかの判断
  • 次の工程へ送ること: コンストラクタが実ファイルに触れるかどうかという、今日の見極めそのものを、次はどう自分の言葉にするか
  • 私が引き受けたこと: 新しく起こすものの置き場所(新しい判定を既存クラスの中に置くか、外の新しいクラスに起こすか)を自分で決めてよい
comments powered by Disqus
システム開発・AIワークフローのご相談は Meetsource へ
Hugo で構築されています。
テーマ Stack は Jimmy によって設計されています。