第1幕: 検分 ── 大黒柱のそば
昼下がりの帳場は、大黒柱のそばがいちばん明るい。
素屋根の隙間から差す光が、柱の肌の、手で撫でられて黒光りした高さのあたりに当たっていて、鉋屑が一片、その光の中をゆっくり降りていた。帳場の文机は柱のすぐ脇にある。今日はその机の上に、端末の画面を打ち出した紙が十数枚、広がっていた。クロベさんとナギさんが並んで座り、クロベさんは鉛筆で、紙の欄外に何かを書きつけている。ナギさんは、書かれた所を隣から目で追っていた。
私は帳場の入口で足を止めて、紙の端を見た。声をかける前に、しばらく眺めてしまった。それから用件を切り出した。
「来月から、年の決算は私が預かることになりました」
二人が顔を上げた。クロベさんは鉛筆を止めたが、目は紙に残っていた。
「店主から、頼まれています。決算の中の、掛売りの回収見込みを引いておく所だけは、私が自分の言葉で説明できるようにしてほしい、と。どこまで直してもらうかも、私が決めてよいそうです」
「引当ですな」
クロベさんが、紙から目を上げないまま言った。
「引当というのは、掛売りのうち、回収できないかもしれない分を見込んで、あらかじめ差し引いておく金額のことです。決算は五つの区切りに分けて数えていて、引当はその三つ目です。この区切りを、段と呼んでいます」
「店主に話すときは、どう言えばいいでしょう」
「回収できないかもしれない分を、先に引いておく額。それで足ります」
私は野帳に、その一行をそのまま書いた。
決算は、一つの関数になっている。クロベさんが、机の上の紙を一枚、私のほうへ滑らせた。
「本物は数百行あります。今日、検めに使うのは、形を保ったまま縮めた写しです」
| |
売上の明細と税率と、決算日までの日数を受け取る。現金の売りと掛売りに分けて集め、税を引き、掛売りの回収見込みを引当として引き、手数料を引いて、最後に報告の文字列を組む。取り消された売りは、一つ目の段のnextで数えずに飛ばす。私にも、筋は追えた。
最後の行のwantarrayは、この関数を呼んだ側が結果をどう受け取ろうとしているかを、関数の中から見るための組み込みだった。呼ぶ側が並びで受け取る形(Perlでは「リスト文脈」と呼ぶ)なら純額と報告の二つを、一つの変数で受け取る形(「スカラ文脈」)なら報告だけを返す。クロベさんは、呼び出しの側の二行も見せた。
| |
「呼び出しは、この二通りが、実際にあります」
クロベさんは、紙の欄外を鉛筆の頭で示した。そこには、すでに表が書き込まれていた。段ごとに、その段が読む値と、書く値が並んでいる。
| 段 | 読む値 | 書く値 |
|---|---|---|
| 1 集める | sales | cash, credit, lines |
| 2 税 | cash, credit, rate | gross, tax, net |
| 3 引当 | credit, cash, gross, net, day | rec, aged, bad, cap, reserve, net |
| 4 手数料 | net | fee, net |
| 5 報告 | net, lines | report |
私が来る前に、書いてあった。
「大物は、手を入れる前に、どこで何を読んで、どこへ書くかを、先に紙に取ります」
理由は、それだけだった。ナギさんも、驚いた様子は見せなかった。三人で仕事をしていると、そういう紙が先に机に載っていることが、ときどきある。
クロベさんは、紙を見たまま、この作りが今何を支えているかを言った。
「途中の値は、全部この関数の中に閉じています。年に一度、通している間、外から誰にも書き換えられずに済んでいる。この閉じ方が、今日まで決算を守ってきました」
それから、表の三つ目の行に、鉛筆の頭を置いた。
「ナギさん、三つ目だけ、走らせてみてください」
ナギさんが端末に手を伸ばしかけた。クロベさんは、私のほうを見た。
「シオリさんが預かる以上、聞くより、見ていただいたほうが早いですから」
第2幕: 手が入らない ── 三つ目に届かない
ナギさんは、決算を呼ぶ入力を作りはじめた。
三つ目に届くには、手前の二つの段を通らなければならない。売りの明細を作って、税率を決めて、日数を決める。掛売りの入った明細でなければ、引当は零のままだ。ナギさんは入力を一組作って呼び、結果を見て、別の一組を作った。
| |
| |
戻ってくるのは、純額と報告の二つだけだった。ナギさんは明細に現金五千円の売りを足し、日数を三十にして呼び、三十一にして呼んだ。手前の二つの段が同じなら、差は三つ目から来るはずだ。純額は、五三二七円と五二四七円になった。八十円の違いが出る。
私は、画面を横から見ていた。
「その八十円は、三つ目の値ですか」
「引当が増えた分、のはずです」
ナギさんは、そう答えてから、少し間を置いた。
「でも、そのあとに手数料の段があって、純額はそこでも変わります。三つ目が出した値は、純額の中に混ざっていて、取り出せません」
手は止まっていなかった。入力を作り、呼び、結果を見る。それを何度も繰り返している。ただ、確かめたい値だけが、関数の外へ出てこない。外へ出るのは戻り値だけだった。
「三つ目の段だけ、走らせられないんですか」
私が言うと、クロベさんは表の三つ目の欄を見た。
「走らせることは、できます。ただ、三つ目だけを、というのは、今の作りのままではできません。この関数は、一つ目から五つ目まで、必ず通して走ります」
クロベさんは、表の三つ目の行を、鉛筆の頭で順に指した。
「三つ目が読むのは、五つ。書くのは、六つ。純額は、読んで、書き直しますから、両方に数えて、合わせて十一。受け渡しの口が、十一あるということです」
ナギさんが、画面から顔を上げた。
「切り出せば、いいんじゃないですか。三つ目を、別の sub にして。それなら、単独で呼べます」
クロベさんは、否定しなかった。
「書いてみてください」
ナギさんが書いた。三つ目の中身をそのまま別の関数に移し、読む五つを引数にし、書く六つを戻り値にした。
| |
呼び出す側は、決算の関数の中で、この六つを順番どおりに受け取る。動かすと、元の決算と同じ値が出た。
「動きます」
クロベさんが言った。
「引数が五つ、戻り値が六つです。しかも、どれも数です。creditとcashを、逆に渡してみてください」
ナギさんは、呼び出しの行の二つの引数を入れ替えて走らせた。
| |
「警告も、エラーも、出ません。数として合っていれば、Perlは何も言いません」
クロベさんは、それだけ言った。
「切り出した先でも、recやbadを外から見たければ、戻り値に入れるしかありません。入れれば、六つです。一つだけなら、切り出しは動きます。ただ、決算は段が五つあります。五つとも切り出せば、段と段のあいだの受け渡しが、そのたびに口になります。位置で渡す引数を、毎回数えて合わせる仕事です」
クロベさんは、そこで言葉を切ってから、続けた。
「値ごと一つのクラスに持たせれば、段のあいだの口は要らなくなります。どの段も、同じ持ち物を、名前で読み書きします」
ナギさんの手が、一度止まった。それから、別の案を出した。
「じゃあ、sub の中に sub を書いたら、どうでしょう。中の sub なら、外側のmyの値が見えます」
書いて、走らせた。三つ目に当たる部分を、決算の関数の中に、名前を付けた sub として置いた。
| |
| |
出力より先に、警告が四行出た。
三回呼んで、三回とも同じ値だった。動いているのに、二度目からの値が古い。ナギさんは、画面の値とキーボードを、交互に見た。道具箱のほうへ伸ばしかけた手を、途中で引いた。
「名前を付けた sub は、外側の関数が最初に呼ばれたときの変数にだけ、繋がります」
クロベさんが、走らせたあとで言った。
「二度目からは、外側の変数は作り直されます。内側の sub は、最初のものを見続けている。警告は、そのことを先に教えていました」
それから、付け加えた。
「sub を変数に入れる書き方なら、呼ぶたびに正しい値が出ます。ただ、その変数は関数の中のmyです。関数の外からは、名前でも触れません」
三つ目だけを外から検める道は、どちらにも無かった。私は、野帳に、切り出し、入れ子、と書いて、それぞれの脇に一行ずつ足した。動くが受け渡しが増える。値が古い。
「決算の全部に、手を入れることになるんでしょうか」
私は、野帳から顔を上げて言った。
「私が頼むとしたら、どこまでを、一度に頼めばいいんでしょう」
「頼み方は、直す形が見えてから決めましょう」
クロベさんは、鉛筆を机に置いた。
「先に、直し方です」
ナギさんが、道具箱の蓋を開ける音がした。
第3幕: 手立て ── 柱は建てたまま
クロベさんは、ここまでの三つを、順に短く言った。
「値が関数の中に閉じているから、外から見えません。切り出せば、受け渡しが増えます。入れ子にしても、古い値しか見えません。だったら、閉じている値のほうを、関数の外へ持ち出します」
それから、手立てを言った。
「長い関数を、一つのクラスに、丸ごと移します。関数の局所変数は、そのクラスのオブジェクトのフィールドにします。元の関数は、オブジェクトを一つ作ってrunを呼ぶだけの形で残します。移し終えてから、runの中身を、段ごとのメソッドに割ります」
局所変数がフィールドになれば、関数が終わったあとも、その値はオブジェクトの中に残る。外から読める。
「メソッドオブジェクトの抽出。Break Out Method Object と呼ばれる技法です」
Michael Feathers の『Working Effectively with Legacy Code』に載っている技法だと、クロベさんは言った。巨大な関数を新しいクラスへ移し、局所変数をフィールドにすると、途中の値を通して確かめられるようになる。そのかわり、割ったメソッドを単独で呼んだとき、フィールドが今何を持っているか分かりにくい、という弱点も、同じ本が認めている。私は、その二つを、野帳に分けて書いた。
「外した後に、同じ結果が出ることは、どうやって分かりますか」
私が訊いても、クロベさんは間を置かなかった。
「先に、今の戻り値を全部、書き取ります。直したあとで、同じ検めを、同じ形で通します」
仕様化テスト、と言った。正しい仕様ではなく、今の振る舞いをそのまま書き取った検めのことだ。ナギさんが、検めのファイルを開いた。
| |
並びで受け取る場合と、一つで受け取る場合の両方を、同じ入力で書き取っている。日数が三十と三十一、掛売りが現金と同じ場合と多い場合、引当が上限に当たる場合と手前の場合。三つ目の境目は、両側を取った。期待値は、正しいはずの値ではなく、今の関数が返した値の写しだ。日数の三十と三十一は$day > 30、掛売りと現金は$credit > $cash、上限は$reserve > $capという、コードの三つの条件に、それぞれ対応している。
表の十行を、並びと一つの二通りで検めて二十本。ほかに、明細を書き換えないこと、戻り値が二つだけであること、戻り値に引当の名前が出ないことを見る三本を足して、全部で二十三本になる。
| |
この手立てが本来頼りにしているものが、Perlには無い。型のある言語なら、関数を移した途端に、まだ元の変数を使っている所を、コンパイラが赤く教えてくれる。Perlは、動かすまで何も言わない。だから、検めを先に書く。
まず丸ごと移して、割るのはそのあとにする、とクロベさんは言った。移している間は式に手を入れないので、検めが緑のままかどうかで、移し損ねを見つけられる。
ナギさんが、新しいクラスを書きはじめた。入力の三つと、これまで局所変数だった値のすべてを、フィールドとして持つ。
| |
Hash::Util::lock_keysは、Perlに最初から付いているモジュールの関数で、ハッシュに、今ある名前以外のキーを足せないようにする。名前を綴り間違えると、その場で止まる。
「これが、コンパイラの代わりです」
クロベさんが言った。
| |
| |
錠が無ければ、grossと書くつもりでtotlと書いても、Perlは何も言わない。grossは零のままで、別のキーが一つ増えるだけだ。錠があれば、書いたときも、読んだときも、その場で止まる。
ナギさんは、決算の関数の中身を、runという名前のメソッドへそのまま写した。局所変数のmy $cashを、$self->{cash}へ、一つずつ置き換えていく。
| |
式の手順には、手を入れなかった。置き換えたのは、変数の書き方だけだった。
写し終えて、元の関数の中身を、これだけにした。
| |
名前も、引数も、戻り値も、これまでと変わらない。ナギさんは、さっきの検めを、そのまま走らせた。
| |
二十三本、緑だった。検めのファイルは、一字も書き換えていない。
呼び出しの側の二行は、bin/kessan_run.plのままだ。並びで受け取る行も、一つで受け取る行も、これまでと同じに動く。組み立てているのは、元の関数の中の、この一行だけだった。決算の式の側は、計算の手順を変えていない。足したのは、新しいクラスと、useの一行だ。
「柱は、建てたままにします」
クロベさんが、大黒柱のほうを、ちらりと見た。
「周りの梁や貫を外して、組み直す。宮大工の言い方では、半解体です。柱は、元の関数の呼び口――名前と引数と、戻り値です。そこは、建てたまま動かしていません。外した周りは、元の関数の外の、新しいクラスに置きます。元の関数に残るのは、そのクラスへ繋ぐ一行の取次ぎだけです。外す、というのは、関数の中身をクラスへ移すこと。組み直す、というのは、移したものを段ごとに割ることです」
私は、その最後の言葉を、野帳に書き写した。帳場を、今日も店の人が通る。柱の周りには、通り道の分の養生が、板で組んであった。
「移す前の名前は、変えません。第1幕の表に書いた値の名前が、そのままフィールドの名前になります。cashはcashのまま、reserveはreserveのままです。検めがrecやbadを名前で読めるのも、そのおかげです」
クロベさんは、ナギさんの画面を指した。
「外した部材に、もとの位置を書いた札を付けます。番付札です。今日フィールドに付けた札は、この名前です」
頼み方の頁を、私は野帳に開いたままにしておいた。
第4幕: 検め ── 割る所と割らない所
「直結を、わざと書き換えてください」
クロベさんが言った。
「結果をいったん変数に受けてから、返す形に」
ナギさんが、元の関数の一行を、二行にした。
| |
検めを走らせると、赤い行がいくつも出た。
| |
落ちたのは、並びで受け取る側の検めだった。一つで受け取る側は、緑のままだ。
「一度、変数に受けると」
クロベさんが言った。
「呼ぶ側が並びで受け取るのか、一つで受け取るのかという情報が、そこで無くなります。my $r = ...と書いた時点で、runは、いつも一つで受け取る形で呼ばれた扱いになります。return f();と書けば、この関数を呼んだ側の受け取り方が、そのままfに伝わります。だから、直結でなければなりません」
ナギさんが、一行に書き直した。検めは、二十三本、緑になった。
「移すのは、ここまでです」
クロベさんが、私のほうを向いた。
「割るのは、これからです。割るたびに、同じ検めを通せます。ここから先は、シオリさんが決めてください。どこまで割るか」
私は、野帳の頁を繰って、決算の五つの段を書いた行を開いた。段の名前が、五つ並んでいる。集める、税、引当、手数料、報告。
それから、クロベさんの顔を見て、頼んだ。
「三つ目までを割ってください。集める、税、引当の三つです。手数料と報告は、割らずに、ひとまとまりで置いておいてください。割った所ごとに、同じ検めが緑なら、そこで引き取ります」
一息おいて、理由を足した。
「私が説明するのは、三つ目までです。割った数だけ、私が読む場所が増えます。緑を確かめられる量までに、しておきます」
クロベさんは、訂正しなかった。
「承りました」
それだけだった。ナギさんが、runの中身を、三つに割りはじめた。
| |
collectの中には、取り消しを飛ばすnextと、それを回すループが、そのまま入っている。ループの本体を二つのメソッドに分けなかったので、nextは、ループの中にあるままだ。式は、写したときと一字も違わない。
検めは、二十三本、緑だった。
ここまでの形を、私は野帳に一枚の図に描いた。移す前は、五つの段と局所変数が一つの関数の中に閉じていて、外へ出るのは戻り値だけだった。移した後は、元の関数は取次ぎの一行だけになり、値はオブジェクトが持って、割った三つのメソッドが順に使う。柱として残した所、つまり今日は手を付けていない所は、薄い灰色の破線で描いた。

「それでは」
クロベさんが、ナギさんのほうへ体を向けた。
「最初の頼みを、もう一度」
ナギさんは、決算の関数を呼ぶ代わりに、新しいクラスのオブジェクトを作った。手前の段が置くはずの値を、自分の手で置いてから、三つ目のメソッドだけを呼ぶ。
| |
| |
三つ目が出した六つの値が、そのまま読めた。日数が三十一で、掛売りのすべてが期日を過ぎた。貸倒れの見込みは百円。上限の三百円には届かないので、引当は百円になり、純額はそのぶん減る。
私は、その六つを、野帳に書き写した。説明する言葉が、初めて手元に揃った。
切り出した版では、前の段の値を、位置で数えて渡していた。ここでは、値を置く行に、名前が書いてある。名前を綴り違えれば、錠が止める。
「前の段が置く値を、先に置いてから呼びました」
クロベさんが言った。
「三つ目だけを走らせるには、この手順が要ります。ここで置いた値が、本当に手前の段が作る値と合っているかは、この検めでは分かりません」
ナギさんは、境目の両側を、七つ並べた。日数が三十と三十一。掛売りと現金が同じ場合と、多い場合。引当が上限の手前と、ちょうどと、超える場合。それぞれ、六つの値をまとめて一本で検めた。ほかに、手数料に触れないことと、綴りの誤りが止まることを見る四本を足して、単独の検めは十一本になる。
| |
クロベさんは、言えることと言えないことを、順に並べた。
「元の関数の名前と引数と戻り値は、変わっていません。並びで受け取っても、一つで受け取っても、これまでと同じ値を返します。三つ目だけを単独で検められて、途中の値を読めます」
それから、少し間を置いた。
「言わないことも、あります。一つ、段の中の計算が正しいことは、保証していません。今の振る舞いを、そのまま写しただけです。二つ、単独の検めは、前の段の値を手で置きます。置いた値が、現実に手前の段が作る値と合っているかは、別の話です。三つ、同じオブジェクトでrunを二度呼ぶと、前の値が残ります。合計に足し込み、明細の一覧に書き足す作りなので、二度目は一度目の続きから始まってしまいます。呼ぶたびに、新しく作ります。四つ、手数料と報告は、割っていません。この二つは、通しの検めだけで確かめています。五つ、元の関数が$_[0]のように引数を直接書き換えていたら、この移し方では同じになりません。引数は、呼んだ側の変数の別名なので、書き換えれば呼んだ側の変数まで変わります。オブジェクトへ渡すと値はコピーされ、その変化は起きなくなるからです。この決算の関数は、my (...) = @_;で先にコピーしてから使っているので、その心配はありません。六つ、設計が良くなったわけではありません。長い関数が、一つのクラスに移っただけで、割っていない所は、これまでと同じ形です」
「七つ」
クロベさんは、ゆっくり言った。
「一度動かした話です。年に一度の決算が、毎年間違えないという保証では、ありません」
私は、その七つを、野帳の同じ頁に書き足した。
日が傾いて、大黒柱の根元まで、西日が届いていた。ナギさんは、自分の帳面を開き、明日の朝いちばんに通す検めの名前を二つ、欄外に書き付けた。私は帳場の灯りをつけた。
普請控
- 見立て: 年に一度の決算の関数は、途中の値がすべて関数の中に閉じていて、三つ目の段(引当)だけを確かめる手立てが無かった。確かめられるのは戻り値の純額と報告だけで、引当の値は、税と手数料に挟まれて、純額の中に混ざっていた
- 手を入れた所: メソッドオブジェクトの抽出(Break Out Method Object)を使った。決算の関数の中身を
Daikoku::Kessan::Settleへ移し、局所変数をフィールドにして、集める・税・引当の三つに割った。元のsettleは、それを組んで呼ぶ一行だけを残した - 触っていない所:
settleの名前、引数の三つ、戻り値の形(並びで受け取ったときと、一つで受け取ったときの両方)は、変えていない。呼び出し元のbin/kessan_run.plの二か所も、そのまま。各段の式の中身は、局所変数をフィールドに置き換えただけで、計算の手順は書き換えていない。手数料と報告の二段は、割らずにfinishにひとまとまりで残した - 次の工程へ送ること: 手数料と報告の二段を割るかどうかは、今日は決めない。割った所ごとに検めが緑なのを確かめて引き取る、という進め方を、続きでも守る。割るほど、私が読む場所が増えるので、確かめられる量に収めるため
- 私が引き受けたこと: 大物の直しを、割る所と割らない所に分けて、私から頼める。今日は、集める・税・引当の三つまでを割り、手数料と報告は割らないと決めて、その理由(私が説明できるのは三つ目まで。割った数だけ、私が読む場所が増える)を添えて、クロベさんに頼んだ。頼んだ範囲を引き取れるかどうかは、割った所ごとに、挙動が移す前と同じかどうかを、検めの緑で私が確かめる
