Featured image of post 数百行の関数の「途中だけ」をテストしたい。関数をクラスへ移して段ごとに割る【Break Out Method Object】

数百行の関数の「途中だけ」をテストしたい。関数をクラスへ移して段ごとに割る【Break Out Method Object】

数百行の決算処理は途中の値がすべて関数の中に閉じていて、3段目だけを確かめられない。関数をクラスへ移し、局所変数をフィールドにする――メソッドオブジェクトの抽出(Break Out Method Object)を、呼び方も戻り値も変えずに行います。

第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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package Daikoku::Kessan;
use strict;
use warnings;

sub settle {
    my ($sales, $rate, $day) = @_;
    my $cash    = 0;
    my $credit  = 0;
    my $gross   = 0;
    my $tax     = 0;
    my $net     = 0;
    my $rec     = 0;
    my $aged    = 0;
    my $bad     = 0;
    my $cap     = 0;
    my $reserve = 0;
    my $fee     = 0;
    my $report  = '';
    my @lines;

    # 1. 集める
    for my $s (@$sales) {
        next if $s->{void};
        if ($s->{kind} eq 'cash') {
            $cash += $s->{amt};
        } else {
            $credit += $s->{amt};
        }
        push @lines, $s->{id};
    }

    # 2. 税
    $gross = $cash + $credit;
    $tax   = int($gross * $rate);
    $net   = $gross - $tax;

    # 3. 引当
    $rec     = $credit;
    $aged    = $day > 30 ? $rec : 0;
    $bad     = int($aged * 0.10) + int(($rec - $aged) * 0.02);
    $cap     = int($gross * 0.05);
    $reserve = $bad + ($credit > $cash ? 10 : 0);
    $reserve = $cap if $reserve > $cap;
    $net    -= $reserve;

    # 4. 手数料
    $fee  = int($net * 0.01);
    $net -= $fee;

    # 5. 報告
    $report = "net=$net lines=" . scalar(@lines);

    return wantarray ? ($net, $report) : $report;
}

1;

売上の明細と税率と、決算日までの日数を受け取る。現金の売りと掛売りに分けて集め、税を引き、掛売りの回収見込みを引当として引き、手数料を引いて、最後に報告の文字列を組む。取り消された売りは、一つ目の段のnextで数えずに飛ばす。私にも、筋は追えた。

最後の行のwantarrayは、この関数を呼んだ側が結果をどう受け取ろうとしているかを、関数の中から見るための組み込みだった。呼ぶ側が並びで受け取る形(Perlでは「リスト文脈」と呼ぶ)なら純額と報告の二つを、一つの変数で受け取る形(「スカラ文脈」)なら報告だけを返す。クロベさんは、呼び出しの側の二行も見せた。

1
2
my ($net, $report) = Daikoku::Kessan::settle($sales, 0.1, 45);   # 並びで受け取る
my $report2        = Daikoku::Kessan::settle($sales, 0.1, 45);   # 一つで受け取る

「呼び出しは、この二通りが、実際にあります」

クロベさんは、紙の欄外を鉛筆の頭で示した。そこには、すでに表が書き込まれていた。段ごとに、その段が読む値と、書く値が並んでいる。

段読む値書く値
1 集めるsalescash, credit, lines
2 税cash, credit, rategross, tax, net
3 引当credit, cash, gross, net, dayrec, aged, bad, cap, reserve, net
4 手数料netfee, net
5 報告net, linesreport

私が来る前に、書いてあった。

「大物は、手を入れる前に、どこで何を読んで、どこへ書くかを、先に紙に取ります」

理由は、それだけだった。ナギさんも、驚いた様子は見せなかった。三人で仕事をしていると、そういう紙が先に机に載っていることが、ときどきある。

クロベさんは、紙を見たまま、この作りが今何を支えているかを言った。

「途中の値は、全部この関数の中に閉じています。年に一度、通している間、外から誰にも書き換えられずに済んでいる。この閉じ方が、今日まで決算を守ってきました」

それから、表の三つ目の行に、鉛筆の頭を置いた。

「ナギさん、三つ目だけ、走らせてみてください」

ナギさんが端末に手を伸ばしかけた。クロベさんは、私のほうを見た。

「シオリさんが預かる以上、聞くより、見ていただいたほうが早いですから」

第2幕: 手が入らない ── 三つ目に届かない

ナギさんは、決算を呼ぶ入力を作りはじめた。

三つ目に届くには、手前の二つの段を通らなければならない。売りの明細を作って、税率を決めて、日数を決める。掛売りの入った明細でなければ、引当は零のままだ。ナギさんは入力を一組作って呼び、結果を見て、別の一組を作った。

1
2
my @r = Daikoku::Kessan::settle([ { id => 'G1', kind => 'credit', amt => 1000, void => 0 } ], 0.1, 31);
print "@r\n";
1
842 net=842 lines=1

戻ってくるのは、純額と報告の二つだけだった。ナギさんは明細に現金五千円の売りを足し、日数を三十にして呼び、三十一にして呼んだ。手前の二つの段が同じなら、差は三つ目から来るはずだ。純額は、五三二七円と五二四七円になった。八十円の違いが出る。

私は、画面を横から見ていた。

「その八十円は、三つ目の値ですか」

「引当が増えた分、のはずです」

ナギさんは、そう答えてから、少し間を置いた。

「でも、そのあとに手数料の段があって、純額はそこでも変わります。三つ目が出した値は、純額の中に混ざっていて、取り出せません」

手は止まっていなかった。入力を作り、呼び、結果を見る。それを何度も繰り返している。ただ、確かめたい値だけが、関数の外へ出てこない。外へ出るのは戻り値だけだった。

「三つ目の段だけ、走らせられないんですか」

私が言うと、クロベさんは表の三つ目の欄を見た。

「走らせることは、できます。ただ、三つ目だけを、というのは、今の作りのままではできません。この関数は、一つ目から五つ目まで、必ず通して走ります」

クロベさんは、表の三つ目の行を、鉛筆の頭で順に指した。

「三つ目が読むのは、五つ。書くのは、六つ。純額は、読んで、書き直しますから、両方に数えて、合わせて十一。受け渡しの口が、十一あるということです」

ナギさんが、画面から顔を上げた。

「切り出せば、いいんじゃないですか。三つ目を、別の sub にして。それなら、単独で呼べます」

クロベさんは、否定しなかった。

「書いてみてください」

ナギさんが書いた。三つ目の中身をそのまま別の関数に移し、読む五つを引数にし、書く六つを戻り値にした。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
sub provision {
    my ($credit, $day, $cash, $gross, $net) = @_;
    my $rec     = $credit;
    my $aged    = $day > 30 ? $rec : 0;
    my $bad     = int($aged * 0.10) + int(($rec - $aged) * 0.02);
    my $cap     = int($gross * 0.05);
    my $reserve = $bad + ($credit > $cash ? 10 : 0);
    $reserve = $cap if $reserve > $cap;
    $net -= $reserve;
    return ($rec, $aged, $bad, $cap, $reserve, $net);
}

呼び出す側は、決算の関数の中で、この六つを順番どおりに受け取る。動かすと、元の決算と同じ値が出た。

「動きます」

クロベさんが言った。

「引数が五つ、戻り値が六つです。しかも、どれも数です。creditとcashを、逆に渡してみてください」

ナギさんは、呼び出しの行の二つの引数を入れ替えて走らせた。

1
2
正しい順:   rec,aged,bad,cap,reserve,net = 1000 1000 100 300 100 5300
取り違え:   rec,aged,bad,cap,reserve,net = 5000 5000 500 300 300 5100

「警告も、エラーも、出ません。数として合っていれば、Perlは何も言いません」

クロベさんは、それだけ言った。

「切り出した先でも、recやbadを外から見たければ、戻り値に入れるしかありません。入れれば、六つです。一つだけなら、切り出しは動きます。ただ、決算は段が五つあります。五つとも切り出せば、段と段のあいだの受け渡しが、そのたびに口になります。位置で渡す引数を、毎回数えて合わせる仕事です」

クロベさんは、そこで言葉を切ってから、続けた。

「値ごと一つのクラスに持たせれば、段のあいだの口は要らなくなります。どの段も、同じ持ち物を、名前で読み書きします」

ナギさんの手が、一度止まった。それから、別の案を出した。

「じゃあ、sub の中に sub を書いたら、どうでしょう。中の sub なら、外側のmyの値が見えます」

書いて、走らせた。三つ目に当たる部分を、決算の関数の中に、名前を付けた sub として置いた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# t/red/1_named_nested.pl
sub settle {
    my ($cash, $credit, $day) = @_;
    my $gross = $cash + $credit;
    sub provision_named {
        my $aged = $day > 30 ? $credit : 0;
        my $bad  = int($aged * 0.10) + int(($credit - $aged) * 0.02);
        my $cap  = int($gross * 0.05);
        my $reserve = $bad + ($credit > $cash ? 10 : 0);
        $reserve = $cap if $reserve > $cap;
        return $reserve;
    }
    return provision_named();
}

my @in = ([5000, 1000, 30], [5000, 2000, 31], [5000, 3000, 31]);
print "named: ", join(',', map { settle(@$_) } @in), "\n";
print "want:  20,200,300\n";
1
2
3
4
5
6
Variable "$day" will not stay shared at t/red/1_named_nested.pl line 10.
Variable "$credit" will not stay shared at t/red/1_named_nested.pl line 10.
Variable "$gross" will not stay shared at t/red/1_named_nested.pl line 12.
Variable "$cash" will not stay shared at t/red/1_named_nested.pl line 13.
named: 20,20,20
want:  20,200,300

出力より先に、警告が四行出た。

三回呼んで、三回とも同じ値だった。動いているのに、二度目からの値が古い。ナギさんは、画面の値とキーボードを、交互に見た。道具箱のほうへ伸ばしかけた手を、途中で引いた。

「名前を付けた sub は、外側の関数が最初に呼ばれたときの変数にだけ、繋がります」

クロベさんが、走らせたあとで言った。

「二度目からは、外側の変数は作り直されます。内側の sub は、最初のものを見続けている。警告は、そのことを先に教えていました」

それから、付け加えた。

「sub を変数に入れる書き方なら、呼ぶたびに正しい値が出ます。ただ、その変数は関数の中のmyです。関数の外からは、名前でも触れません」

三つ目だけを外から検める道は、どちらにも無かった。私は、野帳に、切り出し、入れ子、と書いて、それぞれの脇に一行ずつ足した。動くが受け渡しが増える。値が古い。

「決算の全部に、手を入れることになるんでしょうか」

私は、野帳から顔を上げて言った。

「私が頼むとしたら、どこまでを、一度に頼めばいいんでしょう」

「頼み方は、直す形が見えてから決めましょう」

クロベさんは、鉛筆を机に置いた。

「先に、直し方です」

ナギさんが、道具箱の蓋を開ける音がした。

第3幕: 手立て ── 柱は建てたまま

クロベさんは、ここまでの三つを、順に短く言った。

「値が関数の中に閉じているから、外から見えません。切り出せば、受け渡しが増えます。入れ子にしても、古い値しか見えません。だったら、閉じている値のほうを、関数の外へ持ち出します」

それから、手立てを言った。

「長い関数を、一つのクラスに、丸ごと移します。関数の局所変数は、そのクラスのオブジェクトのフィールドにします。元の関数は、オブジェクトを一つ作ってrunを呼ぶだけの形で残します。移し終えてから、runの中身を、段ごとのメソッドに割ります」

局所変数がフィールドになれば、関数が終わったあとも、その値はオブジェクトの中に残る。外から読める。

「メソッドオブジェクトの抽出。Break Out Method Object と呼ばれる技法です」

Michael Feathers の『Working Effectively with Legacy Code』に載っている技法だと、クロベさんは言った。巨大な関数を新しいクラスへ移し、局所変数をフィールドにすると、途中の値を通して確かめられるようになる。そのかわり、割ったメソッドを単独で呼んだとき、フィールドが今何を持っているか分かりにくい、という弱点も、同じ本が認めている。私は、その二つを、野帳に分けて書いた。

「外した後に、同じ結果が出ることは、どうやって分かりますか」

私が訊いても、クロベさんは間を置かなかった。

「先に、今の戻り値を全部、書き取ります。直したあとで、同じ検めを、同じ形で通します」

仕様化テスト、と言った。正しい仕様ではなく、今の振る舞いをそのまま書き取った検めのことだ。ナギさんが、検めのファイルを開いた。

 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
use strict;
use warnings;
use Test::More;
use Daikoku::Kessan;

sub row { my ($id, $kind, $amt, $void) = @_; return { id => $id, kind => $kind, amt => $amt, void => $void || 0 } }

# [名前, 明細, 税率, 日数, 期待する純額, 期待する報告]
my @table = (
    ['空の明細',                   [],                                                        0.1, 10,    0, 'net=0 lines=0'],
    ['現金だけ',                   [ row('C1', 'cash', 1000) ],                               0.1, 10,  891, 'net=891 lines=1'],
    ['期日 日数30(超過にならない)', [ row('E1', 'cash', 5000), row('E2', 'credit', 1000) ], 0.1, 30, 5327, 'net=5327 lines=2'],
    ['期日 日数31(超過になる)',    [ row('E1', 'cash', 5000), row('E2', 'credit', 1000) ],   0.1, 31, 5247, 'net=5247 lines=2'],
    # ……上限に当たる場合・掛売りが現金より多い場合なども同じ形で並べる(全部で十行)
);

for my $c (@table) {
    my ($name, $sales, $rate, $day, $net, $report) = @$c;
    my @list = Daikoku::Kessan::settle($sales, $rate, $day);
    is_deeply(\@list, [ $net, $report ], "$name: list 文脈は (純額, 報告)");
    my $scalar = Daikoku::Kessan::settle($sales, $rate, $day);
    is($scalar, $report, "$name: scalar 文脈は報告だけ");
}

done_testing;

並びで受け取る場合と、一つで受け取る場合の両方を、同じ入力で書き取っている。日数が三十と三十一、掛売りが現金と同じ場合と多い場合、引当が上限に当たる場合と手前の場合。三つ目の境目は、両側を取った。期待値は、正しいはずの値ではなく、今の関数が返した値の写しだ。日数の三十と三十一は$day > 30、掛売りと現金は$credit > $cash、上限は$reserve > $capという、コードの三つの条件に、それぞれ対応している。

表の十行を、並びと一つの二通りで検めて二十本。ほかに、明細を書き換えないこと、戻り値が二つだけであること、戻り値に引当の名前が出ないことを見る三本を足して、全部で二十三本になる。

1
2
3
4
$ prove -Ilib t/settle_characterize.t
t/settle_characterize.t .. ok
All tests successful.
Files=1, Tests=23, Result: PASS

この手立てが本来頼りにしているものが、Perlには無い。型のある言語なら、関数を移した途端に、まだ元の変数を使っている所を、コンパイラが赤く教えてくれる。Perlは、動かすまで何も言わない。だから、検めを先に書く。

まず丸ごと移して、割るのはそのあとにする、とクロベさんは言った。移している間は式に手を入れないので、検めが緑のままかどうかで、移し損ねを見つけられる。

ナギさんが、新しいクラスを書きはじめた。入力の三つと、これまで局所変数だった値のすべてを、フィールドとして持つ。

 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
package Daikoku::Kessan::Settle;
use strict;
use warnings;
use Hash::Util ();

sub new {
    my ($class, $sales, $rate, $day) = @_;
    my $self = bless {
        sales   => $sales,
        rate    => $rate,
        day     => $day,
        cash    => 0,
        credit  => 0,
        gross   => 0,
        tax     => 0,
        net     => 0,
        rec     => 0,
        aged    => 0,
        bad     => 0,
        cap     => 0,
        reserve => 0,
        fee     => 0,
        report  => '',
        lines   => [],
    }, $class;
    Hash::Util::lock_keys(%$self);
    return $self;
}

Hash::Util::lock_keysは、Perlに最初から付いているモジュールの関数で、ハッシュに、今ある名前以外のキーを足せないようにする。名前を綴り間違えると、その場で止まる。

「これが、コンパイラの代わりです」

クロベさんが言った。

1
2
# t/red/4_typo.pl($free は錠を掛けていない複製、$locked は錠を掛けた本物)
$free->{totl} += 5;      # 綴りの誤り。錠が無いと、警告もなく別のキーが生まれる
1
2
3
lock_keys 無し: gross=0 keys=gross,totl
lock_keys 有り(書き込み): Attempt to access disallowed key 'totl' in a restricted hash at t/red/4_typo.pl line 13.
lock_keys 有り(読み出し): Attempt to access disallowed key 'totl' in a restricted hash at t/red/4_typo.pl line 14.

錠が無ければ、grossと書くつもりでtotlと書いても、Perlは何も言わない。grossは零のままで、別のキーが一つ増えるだけだ。錠があれば、書いたときも、読んだときも、その場で止まる。

ナギさんは、決算の関数の中身を、runという名前のメソッドへそのまま写した。局所変数のmy $cashを、$self->{cash}へ、一つずつ置き換えていく。

1
2
$cash += $s->{amt};            # 元の関数(局所変数)
$self->{cash} += $s->{amt};    # 移した先(フィールド)

式の手順には、手を入れなかった。置き換えたのは、変数の書き方だけだった。

写し終えて、元の関数の中身を、これだけにした。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package Daikoku::Kessan;
use strict;
use warnings;
use Daikoku::Kessan::Settle;

sub settle {
    return Daikoku::Kessan::Settle->new(@_)->run;
}

1;

名前も、引数も、戻り値も、これまでと変わらない。ナギさんは、さっきの検めを、そのまま走らせた。

1
2
3
t/settle_characterize.t .. ok
All tests successful.
Files=1, Tests=23, Result: PASS

二十三本、緑だった。検めのファイルは、一字も書き換えていない。

呼び出しの側の二行は、bin/kessan_run.plのままだ。並びで受け取る行も、一つで受け取る行も、これまでと同じに動く。組み立てているのは、元の関数の中の、この一行だけだった。決算の式の側は、計算の手順を変えていない。足したのは、新しいクラスと、useの一行だ。

「柱は、建てたままにします」

クロベさんが、大黒柱のほうを、ちらりと見た。

「周りの梁や貫を外して、組み直す。宮大工の言い方では、半解体です。柱は、元の関数の呼び口――名前と引数と、戻り値です。そこは、建てたまま動かしていません。外した周りは、元の関数の外の、新しいクラスに置きます。元の関数に残るのは、そのクラスへ繋ぐ一行の取次ぎだけです。外す、というのは、関数の中身をクラスへ移すこと。組み直す、というのは、移したものを段ごとに割ることです」

私は、その最後の言葉を、野帳に書き写した。帳場を、今日も店の人が通る。柱の周りには、通り道の分の養生が、板で組んであった。

「移す前の名前は、変えません。第1幕の表に書いた値の名前が、そのままフィールドの名前になります。cashはcashのまま、reserveはreserveのままです。検めがrecやbadを名前で読めるのも、そのおかげです」

クロベさんは、ナギさんの画面を指した。

「外した部材に、もとの位置を書いた札を付けます。番付札です。今日フィールドに付けた札は、この名前です」

頼み方の頁を、私は野帳に開いたままにしておいた。

第4幕: 検め ── 割る所と割らない所

「直結を、わざと書き換えてください」

クロベさんが言った。

「結果をいったん変数に受けてから、返す形に」

ナギさんが、元の関数の一行を、二行にした。

1
2
3
4
sub settle {
    my $r = Daikoku::Kessan::Settle->new(@_)->run;
    return $r;
}

検めを走らせると、赤い行がいくつも出た。

1
2
3
#   Failed test '空の明細: list 文脈は (純額, 報告)'
#     $got->[0] = 'net=0 lines=0'
#     $expected->[0] = '0'

落ちたのは、並びで受け取る側の検めだった。一つで受け取る側は、緑のままだ。

「一度、変数に受けると」

クロベさんが言った。

「呼ぶ側が並びで受け取るのか、一つで受け取るのかという情報が、そこで無くなります。my $r = ...と書いた時点で、runは、いつも一つで受け取る形で呼ばれた扱いになります。return f();と書けば、この関数を呼んだ側の受け取り方が、そのままfに伝わります。だから、直結でなければなりません」

ナギさんが、一行に書き直した。検めは、二十三本、緑になった。

「移すのは、ここまでです」

クロベさんが、私のほうを向いた。

「割るのは、これからです。割るたびに、同じ検めを通せます。ここから先は、シオリさんが決めてください。どこまで割るか」

私は、野帳の頁を繰って、決算の五つの段を書いた行を開いた。段の名前が、五つ並んでいる。集める、税、引当、手数料、報告。

それから、クロベさんの顔を見て、頼んだ。

「三つ目までを割ってください。集める、税、引当の三つです。手数料と報告は、割らずに、ひとまとまりで置いておいてください。割った所ごとに、同じ検めが緑なら、そこで引き取ります」

一息おいて、理由を足した。

「私が説明するのは、三つ目までです。割った数だけ、私が読む場所が増えます。緑を確かめられる量までに、しておきます」

クロベさんは、訂正しなかった。

「承りました」

それだけだった。ナギさんが、runの中身を、三つに割りはじめた。

 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
42
43
44
45
46
47
48
49
50
sub run {
    my ($self) = @_;
    $self->collect;
    $self->tax;
    $self->provision;
    $self->finish;
    return wantarray ? ($self->{net}, $self->{report}) : $self->{report};
}

# 1. 集める
sub collect {
    my ($self) = @_;
    for my $s (@{ $self->{sales} }) {
        next if $s->{void};
        if ($s->{kind} eq 'cash') {
            $self->{cash} += $s->{amt};
        } else {
            $self->{credit} += $s->{amt};
        }
        push @{ $self->{lines} }, $s->{id};
    }
}

# 2. 税
sub tax {
    my ($self) = @_;
    $self->{gross} = $self->{cash} + $self->{credit};
    $self->{tax}   = int($self->{gross} * $self->{rate});
    $self->{net}   = $self->{gross} - $self->{tax};
}

# 3. 引当
sub provision {
    my ($self) = @_;
    $self->{rec}     = $self->{credit};
    $self->{aged}    = $self->{day} > 30 ? $self->{rec} : 0;
    $self->{bad}     = int($self->{aged} * 0.10) + int(($self->{rec} - $self->{aged}) * 0.02);
    $self->{cap}     = int($self->{gross} * 0.05);
    $self->{reserve} = $self->{bad} + ($self->{credit} > $self->{cash} ? 10 : 0);
    $self->{reserve} = $self->{cap} if $self->{reserve} > $self->{cap};
    $self->{net}    -= $self->{reserve};
}

# 4. 手数料と 5. 報告(割らずにひとまとまり)
sub finish {
    my ($self) = @_;
    $self->{fee}  = int($self->{net} * 0.01);
    $self->{net} -= $self->{fee};
    $self->{report} = "net=$self->{net} lines=" . scalar(@{ $self->{lines} });
}

collectの中には、取り消しを飛ばすnextと、それを回すループが、そのまま入っている。ループの本体を二つのメソッドに分けなかったので、nextは、ループの中にあるままだ。式は、写したときと一字も違わない。

検めは、二十三本、緑だった。

ここまでの形を、私は野帳に一枚の図に描いた。移す前は、五つの段と局所変数が一つの関数の中に閉じていて、外へ出るのは戻り値だけだった。移した後は、元の関数は取次ぎの一行だけになり、値はオブジェクトが持って、割った三つのメソッドが順に使う。柱として残した所、つまり今日は手を付けていない所は、薄い灰色の破線で描いた。

決算の関数を Daikoku::Kessan::Settle オブジェクトへ移す前後の比較図。移す前は五つの段と局所変数が一つの関数に閉じて戻り値しか見えないが、移した後は settle が取次ぎだけを残し、オブジェクトが値を持って collect、tax、provision、finish を順に呼び、provision は前段の値を置けば単独で呼べる。呼び出し元と settle と finish は今回触っていない所として灰色の破線で示している

「それでは」

クロベさんが、ナギさんのほうへ体を向けた。

「最初の頼みを、もう一度」

ナギさんは、決算の関数を呼ぶ代わりに、新しいクラスのオブジェクトを作った。手前の段が置くはずの値を、自分の手で置いてから、三つ目のメソッドだけを呼ぶ。

1
2
3
4
5
my $o = Daikoku::Kessan::Settle->new([], 0.1, 31);
$o->{$_} = { cash => 5000, credit => 1000, gross => 6000, net => 5400 }->{$_}
    for qw(cash credit gross net);
$o->provision;
print join(' ', map { "$_=$o->{$_}" } qw(rec aged bad cap reserve net)), "\n";
1
rec=1000 aged=1000 bad=100 cap=300 reserve=100 net=5300

三つ目が出した六つの値が、そのまま読めた。日数が三十一で、掛売りのすべてが期日を過ぎた。貸倒れの見込みは百円。上限の三百円には届かないので、引当は百円になり、純額はそのぶん減る。

私は、その六つを、野帳に書き写した。説明する言葉が、初めて手元に揃った。

切り出した版では、前の段の値を、位置で数えて渡していた。ここでは、値を置く行に、名前が書いてある。名前を綴り違えれば、錠が止める。

「前の段が置く値を、先に置いてから呼びました」

クロベさんが言った。

「三つ目だけを走らせるには、この手順が要ります。ここで置いた値が、本当に手前の段が作る値と合っているかは、この検めでは分かりません」

ナギさんは、境目の両側を、七つ並べた。日数が三十と三十一。掛売りと現金が同じ場合と、多い場合。引当が上限の手前と、ちょうどと、超える場合。それぞれ、六つの値をまとめて一本で検めた。ほかに、手数料に触れないことと、綴りの誤りが止まることを見る四本を足して、単独の検めは十一本になる。

1
2
3
4
5
$ prove -Iafter/lib t/settle_characterize.t t/settle_provision.t
t/settle_characterize.t .. ok
t/settle_provision.t ..... ok
All tests successful.
Files=2, Tests=34, Result: PASS

クロベさんは、言えることと言えないことを、順に並べた。

「元の関数の名前と引数と戻り値は、変わっていません。並びで受け取っても、一つで受け取っても、これまでと同じ値を返します。三つ目だけを単独で検められて、途中の値を読めます」

それから、少し間を置いた。

「言わないことも、あります。一つ、段の中の計算が正しいことは、保証していません。今の振る舞いを、そのまま写しただけです。二つ、単独の検めは、前の段の値を手で置きます。置いた値が、現実に手前の段が作る値と合っているかは、別の話です。三つ、同じオブジェクトでrunを二度呼ぶと、前の値が残ります。合計に足し込み、明細の一覧に書き足す作りなので、二度目は一度目の続きから始まってしまいます。呼ぶたびに、新しく作ります。四つ、手数料と報告は、割っていません。この二つは、通しの検めだけで確かめています。五つ、元の関数が$_[0]のように引数を直接書き換えていたら、この移し方では同じになりません。引数は、呼んだ側の変数の別名なので、書き換えれば呼んだ側の変数まで変わります。オブジェクトへ渡すと値はコピーされ、その変化は起きなくなるからです。この決算の関数は、my (...) = @_;で先にコピーしてから使っているので、その心配はありません。六つ、設計が良くなったわけではありません。長い関数が、一つのクラスに移っただけで、割っていない所は、これまでと同じ形です」

「七つ」

クロベさんは、ゆっくり言った。

「一度動かした話です。年に一度の決算が、毎年間違えないという保証では、ありません」

私は、その七つを、野帳の同じ頁に書き足した。

日が傾いて、大黒柱の根元まで、西日が届いていた。ナギさんは、自分の帳面を開き、明日の朝いちばんに通す検めの名前を二つ、欄外に書き付けた。私は帳場の灯りをつけた。


普請控

  • 見立て: 年に一度の決算の関数は、途中の値がすべて関数の中に閉じていて、三つ目の段(引当)だけを確かめる手立てが無かった。確かめられるのは戻り値の純額と報告だけで、引当の値は、税と手数料に挟まれて、純額の中に混ざっていた
  • 手を入れた所: メソッドオブジェクトの抽出(Break Out Method Object)を使った。決算の関数の中身をDaikoku::Kessan::Settleへ移し、局所変数をフィールドにして、集める・税・引当の三つに割った。元のsettleは、それを組んで呼ぶ一行だけを残した
  • 触っていない所: settleの名前、引数の三つ、戻り値の形(並びで受け取ったときと、一つで受け取ったときの両方)は、変えていない。呼び出し元のbin/kessan_run.plの二か所も、そのまま。各段の式の中身は、局所変数をフィールドに置き換えただけで、計算の手順は書き換えていない。手数料と報告の二段は、割らずにfinishにひとまとまりで残した
  • 次の工程へ送ること: 手数料と報告の二段を割るかどうかは、今日は決めない。割った所ごとに検めが緑なのを確かめて引き取る、という進め方を、続きでも守る。割るほど、私が読む場所が増えるので、確かめられる量に収めるため
  • 私が引き受けたこと: 大物の直しを、割る所と割らない所に分けて、私から頼める。今日は、集める・税・引当の三つまでを割り、手数料と報告は割らないと決めて、その理由(私が説明できるのは三つ目まで。割った数だけ、私が読む場所が増える)を添えて、クロベさんに頼んだ。頼んだ範囲を引き取れるかどうかは、割った所ごとに、挙動が移す前と同じかどうかを、検めの緑で私が確かめる
comments powered by Disqus
システム開発・AIワークフローのご相談は Meetsource へ
Hugo で構築されています。
テーマ Stack は Jimmy によって設計されています。