数が合わない朝
内線が鳴ったのは、朝の八時十二分だった。
「2階。Aの13番、リストと棚が合わん。リストは40って言うんだが、棚には28しか無いぞ」
現場の班長だ。あの人は数えで間違えたことがない。検品二十年、俺がフォークに乗っていた頃からの付き合いだ。だから俺は、現場を疑う順番を最初から飛ばした。数えなら現場が正しい。なら、嘘をついているのはうちの2階——俺の機械のほうだ。
俺は地場の3PL——荷主の物流をまるごと請け負う倉庫会社の社内SEをやっている。五十四。元は現場で、入出荷を二十年。腰を痛めて事務所に上がってから、Excelのマクロ、Access、と独学でここまで来た。情シスは実質、俺ひとりだ。
調べているうちに、口の中が乾いてきた。荷主の基幹システムは、昨日の在庫で今日の引当——注文へ在庫を割り当てる処理——をかけていた。うちのWMS(倉庫管理システム)から毎晩3時に送る在庫スナップショット——全SKU(品目ごとの管理単位)の現在庫を書き出して荷主へ渡すファイルが、昨夜は届いていない。荷主の営業は、もう存在しない28個を、今朝、売ったことになる。
スケジューラの実行結果一覧を開く。深夜のバッチは3本。仕入先からの入荷実績取込。荷主基幹への出荷実績連携。そして在庫スナップショット。
全部、緑だった。
念のために言っておくと、この緑と赤は、プログラムが終わるときに残す番号——終了コードで決まる。0なら成功で緑、それ以外は失敗で赤。画面は処理の中身を見ていない。番号だけを見ている。
そして俺は思い出していた。夜中の2時、出荷実績連携の「接続失敗・異常終了」の通知で一度起こされたのだ。荷主側の基幹メンテが延びて、接続が切れていた。メンテ明けの5時に手で流し直して、画面が緑に変わるのを確認して——指差して、ヨシ、とまで言って——二度寝した。
在庫スナップショットは3時に動いたことになっている。緑で。
ログを開いた。書いてあった。
| |
ログには、書いてあったんだ。誰も毎朝ログは読まない。読むのは画面の色だ。
緑だった。俺は確認した。確認は正しかった。緑が嘘だった。
荷主の情シス担当に電話して、頭を下げた。引当を止めてもらい、再送の段取りを決める。3PLは数字の信用で飯を食っている。誤出荷の一件より、昨日の数字を今日の数字として渡していたことのほうが、商売の根に刺さる。
電話の終わりに、先方が言った。
「原因の切り分け、手が要るようでしたら——うちが前に世話になった人がいますよ。出張整備の。コードのほうの、ですけど。みんな“親方”って呼んでました」
コードの、整備。聞いたことのない商売だった。だが荷主筋の紹介だ。断る選択肢は、最初から無い。親方、という呼び名だけが妙に腑に落ちた。現場で年長の職人をそう呼ぶ。俺の口にも、馴染みのある言葉だ。
親方が来たのは、昼前だった。
ラフな格好に薄い鞄。女の人だ、というのが最初の感想で、それ以上の感想を持つ前に、名刺より先に口が動いた。「止まっていたのは、どの子です」
機械を「子」と呼ぶ言い方に、おや、と思った。現場の年寄りがやるやつだ。
「……止まらなかったんです。止まるべきやつが」
親方の目が、少しだけ動いたように見えた。
俺は経緯を一気に説明した。三本のバッチ。夜中のメンテ延長。出荷連携は止まって通知してきたこと。スナップショットは黙って緑だったこと。説明しながらスケジューラの画面を見せたが、親方は画面より先に、デスクの黒いバインダーに目を留めた。俺の点検表綴りだ。現場の癖が抜けなくて、機械もコードも、確認事項は紙に書いて指差すことにしている。
「先に、その紙を見せてもらえますか」
めくる手が、途中で二往復した。それから返してよこして、一言。
「これは、続けてください」
褒められたのか、指示されたのか分からなかったが、悪い気はしなかった。だが紙は今朝、役に立たなかった。
コードを見せた。まず、夜中にちゃんと止まって通知してきた側——出荷実績連携だ。
| |
半年前、接続断で入荷取込が黙って空振りする小さい事故があった。それを機に「接続失敗は3回まで試す。ダメなら担当へ通知して、異常終了する」という改修を決めて、入荷と出荷の2本には入れた。log.Fatal は、エラーを記録してプログラムを失敗の番号——非ゼロの終了コードで終わらせる。だから画面が赤になり、通知が飛び、俺は夜中に起こされる。起こされるのは、正しい設計だ。
「入荷取込も、ほぼこの写しです。半年前の直しも入ってます。——問題は、三本目で」
見てほしいのは2点だ。形がところどころ違うこと。そして、さっきの「改修ブロック」が無いこと。
| |
エラーで止まらないんです、こいつ。全部 log.Printf して、return。main が普通に終わるから、終了コードは0。画面は緑。半年前にそれをやめる改修をしたのに、こいつにだけ、入れてない。
「俺のポカです」
親方はそれを、肯定も否定もしなかった。「直しましょう。話はそれからだ」
直し方は、分かっている。さっきの改修ブロックを持ってくればいい。分からんのは——なんで俺は、これを半年も放っておけたのか、のほうだった。
三本目だけ、同じ直しが当たらない
親方の応急処置は、見ものだった。正確には——見ものになるはずが、ならなかったことが、見ものだった。
親方は出荷連携のファイルから、改修ブロック——リトライのループと通知とエラー返しの一塊——を選択してコピーした。スナップショットのファイルを開く。貼る。
手が、止まった。
貼り先が、無いのだ。向こうは conn、こっちは c。向こうは接続のあとに前処理の関数が並ぶが、こっちはべた書きで、行の対応が取れない。エラーの返し方も違う。向こうは err を返して main の log.Fatal に渡すが、こっちの main は何も受け取らない作りだ。
親方は貼るのをやめて、無言で手で書き直しはじめた。まず main を、出荷連携と同じ「main は run() を呼んで、エラーなら log.Fatal」の形に割る。そのうえで dialWMS をリトライのループで包み、通知を呼べる形に揃え、握りつぶしていた return を、エラーを返して非ゼロで終わる形へ一行ずつ直していく。
| |
残りの log.Printf して握りつぶす行も、同じ要領で、一箇所ずつエラーを返す形へ。コピーで済むはずの作業を、形を合わせながら、手で。五分かかった。
その五分の間、俺はずっと親方の手元を見ていた。迷いがない。行く先を全部決めてから指が動いている。フォークのうまい奴の旋回と同じだ。——これは銭の取れる仕事だな、と職人の値踏みで思った。
書き終えて、親方が初めて口を開いた。
「同じ意味の直しを、いま、三つ目の言い方で書きました。一本目と二本目は、写しが揃っていたから同じ形で済んだ。三本目は——揃っていない。だから、当たらない」
直したスナップショットを手で流す。今日の在庫が荷主へ飛ぶ。先方の情シスから「届きました。引当やり直します」と電話が来て、昼過ぎには現場のリストの数が棚と合った。
「今日の取りこぼしは、これで終わりです。——ですが、これで三回目です」
「三回目?」
「同じ意味の直しを、あなたが半年前に二回——入荷と出荷に——書いた。いま私が、三回目を書いた。四本目のバッチができたら、誰かが四回目を書きますか」
四本目は、作る。たぶん。荷主からは月次の請求データ連携の話がもう来ている。そのたびに、また写して、また直しを配って回るのか。
「……配るたびに、紙のリストの行が増えるだけじゃないですか」
口に出してから、自分の言葉に引っかかった。紙のリスト。そうだ、俺は半年前、この配り物のために紙を作ったはずだ。
保留、と俺の字で書いてあった
「さっきの紙を、もう一度」と親方が言った。「半年前の改修のページを」
バインダーの主は日々の点検表で、半年前のページは分厚い綴りの奥に埋もれていた。めくる。「障害対応改修 配布チェック」。俺が作った配布管理のページだ。
入荷実績取込——✓、済。 出荷実績連携——✓、済。 三行目。在庫スナップショット——
「保留(構成が違うため要調査)」。
俺の字で、書いてあった。
忘れていたのは、直すことじゃなかった。保留した理由のほうだ。「構成が違うため要調査」——半年の間に、俺はこの理由を「俺のポカ」という言葉で上書きしていた。そのほうが、形の違うあいつを開けて読み解くより、楽だったからだ。
「あなたは忘れたんじゃない」と親方が言った。「当てられなかったんです。さっき私が当てて見せたでしょう——五分かかった。揃っていれば貼って終わりの直しが、です。あなたのポカじゃない。写しが育って、同じ直しが同じ形で当たらなくなっていた。それが原因です」
慰めの口調ではなかった。事実を読み上げる声だった。だから、効いた。
「見ます」と親方が言って、エディタの画面を三つに割った。入荷、出荷、スナップショット。三本並べて、上から読みながら、同じ意味の行に線を引いていく。接続。前処理。本処理。後処理。クローズ。入荷と出荷は、線が同じ高さに揃って並ぶ。スナップショットだけ、引く場所が上下に散って、線が斜めに乱れた。
「線、ここは乱れますよ」と俺は口を挟んだ。「字が違う。こっちは conn で、こっちは c だ」
「字面じゃなく、意味で引きます。c も conn も、dialWMS も dialShipment も——やっていることは同じ接続だ。意味が同じなら、同じ印」
「形は崩れていても」と親方は続けた。「読んでいくと、同じ手順です。接続、前処理、本処理、後処理、クローズ。三本とも、骨は同じだ。違うのは、骨に付いている肉のほうで」
骨、という言葉で、腑に落ちるものがあった。
「……手順書だ。うちの現場の。検品も積み込みも、手順書の章立ては同じで、機械ごとに工程の中身だけ違う。これは——同じ手順書を、機械ごとに丸写しして、三冊持ってる状態だ」
「そう。そして半年前、あなたは三冊全部に同じ赤字を入れて回る羽目になった。二冊で力尽きた。三冊目は字が崩れていて、どこに赤を入れていいか、分からなかった」
親方は画面を指したまま、名前を口にした。
「これはコピペプログラミング——動いているコードを丸写しして増やすやり方です。一枚写すだけなら、手堅さのうちだ。だが手順の骨格ごと写すと、共通の直しが出るたびに、写しの数だけ同じ直しを配って回ることになる。配り漏れと、ドリフトが静かに積もる」
ドリフト。コピーどうしが手直しのたびに少しずつ形を変えて、もう同じ直しが同じ形で当たらなくなるほど離れていくこと——親方は、それだけ言い添えた。うちの三本目が、まさにそれだった。2年前、急ぎで写したとき、関数に切るのを省いて、変数名もログも自分流に書いた。あの日の省略が、半年前の「保留」を生み、保留が今朝の「黙って緑」になった。
写すのが悪いんじゃない。写したあとの話なんだ。写しは、生き物みたいに育つ。
整備工場で言えば、同じ設計 of 機械を三台並べて、それぞれ別々に手を入れて回るようなものだ。二台までは新型の部品を組み込めたが、構造が変わってしまっていた三台目だけは、部品が合わずに古いまま放置された。

同じ骨格が三回書かれ、改修ブロックは二箇所にしか無い。それが今朝の、絵解きだった。
俺は画面の赤線を眺めて、訊いた。
「骨が同じなら——骨だけ、一本にできんのですか。現場の手順書なら、共通の章は一冊にまとめて、機械ごとの工程だけ差し替える。コードで、それは」
「できます」と親方は言った。「それが定石です」
骨を一本にして、工程だけ差し込む
「やることは、さっきの線の通りです」と親方は言った。「印が引けた行——三本に共通の行は、骨格として一本にまとめる。引けなかった行——機械ごとに違う中身は、穴にして、あとから差し込む」
共通の章は一冊に。機械ごとの工程は、差し込みページに。俺の手順書の言葉で言えば、そういうことだ。
親方がまず書いたのは、穴の宣言だった。
| |
interface というのは、必要なメソッドの一覧だけを決めた約束事だ。ここでは「繋ぐ・整える・本番・後始末」の四つ。この四つを持っていれば、誰でも差し込みページになれる。
約束事の実例は、すぐ隣にもある。Connect が返す io.Closer も標準ライブラリの interface で、中身は「Close() できるもの」という、いちばん小さい約束事だ。
なお、この batch パッケージは共通部品として一本だけ置く。三本のバッチは、それぞれの main から import して使う。
そして骨格——手順書の本体。
| |
defer は「この関数を抜けるとき、必ず最後に実行する」という予約だ。途中のどの工程で失敗しても、接続のクローズだけは骨格が必ず面倒を見る。
なお、やり直すのは繋ぎ直しだけだ。前処理や本処理が落ちたとき、中身の途中からやり直すことはしない。途中再開はまた別の設計の話で、今日の骨格はそこに踏み込まない——通知して、正直に赤になる。それだけを約束する。
これがTemplate Method(テンプレートメソッド)——手順の骨格を1か所に固定し、変わる工程だけを後から差し替えられるようにする技法だ。本来は継承——親の手順書を子がまるごと引き継ぐ仕組み——を持つ言語の定石だが、Goに継承は無い。だから、差し替えたい工程のほうを interface にして、骨格に渡す。骨格が固定で、工程が可変。向きはどの言語でも同じだ。
俺は Run の中身を上から読んで、引っかかった。
「この Run、さっきの三本のどれとも、ちょっとずつ違いますよね。リトライも通知も入ってる。これは、どの写しなんです」
「写しじゃない。三本の赤線を集めて、一本に書き直した。今日からは、これが原本です。写しは、もう作らない」
原本、という言い方が、また現場の言葉だった。図面の原本。手順書の原本。写しを配る運用は、原本を直すたびに写しを回収して差し替える運用だ。それを、うちはコードでやっていた。
次に親方は、スナップショットの差し込みページを書いた。
| |
書き方も、二つだけ覚えれば読める。func (s *snapshotSteps) Connect() の、関数名の前の括弧——これは「この関数は snapshotSteps に紐づく」という Go の流儀で、いわゆるメソッドだ。そして main の &snapshotSteps{} は、その実体をひとつ作って骨格に渡している。
Connect が接続を「返し」つつ、自分でも「持つ」のが目を引いた。訊くと、返すのは閉じる係——骨格の defer Close へ渡す分、持つのは自分の前処理で使う分だという。役割で分けてある。
ちなみに Go では、snapshotSteps が Steps を満たすのに宣言は要らない。四つのメソッドが揃っていれば、それで Steps だ。implements と書く場所は無い。
入荷も出荷も、同じ要領で差し込みページになった。それぞれのファイルから、リトライのループが消え、通知が消え、defer Close が消えた。残ったのは、そのバッチにしか無い工程だけ。
俺はもう一度、スナップショットの新しいファイルを上から読んだ。読んでから、確かめるように訊いた。
「リトライも通知も、こっちのファイルには、もう書いてない。……次にまた共通の直しが出たら、どうなるんです」
「Run を一回直す。三本とも、その場で直り終わる。配って回る紙のリストは、要らない。——半年前のあなたの改修は、この形なら一行の仕事だった」
三冊に赤字を配るんじゃなく、原本を直せば、差し込みページは全部そのまま。保留する余地が、そもそも無い。
それでも俺の中の現場の人間が、最後の確認をした。
「けど、これ、お行儀のいい設計ってやつでしょう。現場のコードで、ほんとにやるんですか」
親方は答える代わりに、小さなコードを見せた。
| |
「Goの標準ライブラリが、まさにこれです。sort.Sort——並べ替えのループ、つまり骨格は、sort が一本だけ持っている。使う側が書くのは Len、Less、Swap の三つの穴だけ。——ソートのループを、あなたは書いたことが無いでしょう。でも、並ぶ」
言われてみれば、書いたことが無い。並べ替えなんて、呼べば並ぶものだと思っていた。骨は最初から、向こうが握っていたのか。
「工程を差し替えるってのは、その」と、もうひとつ気になっていたことを訊いた。「処理を丸ごと取っ替えるのとは、違うんですか」
「丸ごと差し替えるのは Strategy——処理のやり方を、後から丸ごと差し替えられるようにする技法。あれは呼ぶ側が手順を握ったまま、中身を取っ替える。今日のは逆です。手順は骨格が握って離さない。差し替えるのは、枠の中の工程だけ。主導権の場所が違う」
Strategy は工具の差し替え。あれでいくなら、三本の run() は三本のまま残って、中の道具だけ替わる。今日のは run() そのものを一本にした——手順書そのものが現場を仕切っている。呼ばれるのを待つのは、俺の書く差し込みページのほうだ。
「もうひとつ」と親方が言った。「失敗の出口が Run に一本化されたので、どのバッチも、しくじれば必ず赤になります。黙って緑、は構造上できなくなった」
それは、今朝の俺に一番効く話だった。
「……今朝の俺の指差しは、間違ってなかったことになるんですな。信号が正直なら、指差しは仕事をする。嘘の緑を指差して、ヨシって言ってたのが、今朝までの俺で」
「指差しをやめる必要はない。指差しに値する信号にする——それが整備です」
その整備の図面が、頭の中に浮かんだ。 頑丈な制御盤が中央に一台据え付けられ、そこから伸びる接続口に、それぞれの処理に必要なアタッチメントを差し込むイメージだ。

共通の直しは Run の一箇所。バッチを足すときは、差し込みページを一枚書くだけ。
最後に、ずっと胸にあったことを訊いた。
「これ、若手にも書けるようになりますか。俺が、いつまでもいるわけじゃないんで」
「穴は四つだけです。繋ぐ・整える・本番・後始末。骨格を知らなくても、四つ埋めれば動く。——埋めさせてみればいい」
ヨシ、が信じられる緑になる
試運転は、テストでやった。
骨格のテストは、偽物の差し込みページ——呼ばれた順番を記録するだけの Steps——を Run に渡して、約束ごとを一つずつ確かめる。接続、前処理、本処理、後処理、クローズの順で呼ばれること。接続に失敗を仕込めば3回まで試して、通知が一回飛んで、必ず失敗で返ること。途中の工程でしくじっても、クローズだけは必ず走ること。
等価性のテストも書いた。正常な晩なら、三本のバッチが生む結果——送る件数も、スナップショットの中身も——は、整備の前と後で一致する。整備で挙動を変えていないことの、文書化だ。
そして回帰テスト——同じ不具合が再発しないことを、同じ条件を再現して確かめるテストだ。今朝と同じ接続断の条件で、前のスナップショットは黙って成功し、新しいスナップショットは失敗を返す。同じ条件、違う色。なお、前の側のテストが「通る」のは変な話に聞こえるが、あれは「接続断でも緑になる」という現状をそのまま表明として書き留めたもの——今朝の事故の、文書化だ。
| |
「骨格のテストは、この一本きりです」と親方は言った。「バッチが何本増えても、増えるのは差し込みページと、そのテストだけ」
俺は最後に、バインダーを開いた。「障害対応改修 配布チェック」のページ——入荷✓、出荷✓、そして保留——を外して、新しい一枚を綴じた。
骨格 Run: 原本(直しはここへ一回)。 差し込み: 入荷・出荷・在庫。
三冊の手順書が、原本一冊と差し込み三枚になった。紙が、薄くなった。紙が薄くなる整備はいい整備だと、現場で二十年、そう覚えてきた。
階下から、出荷再開の構内放送が聞こえた。コンベアのモーターが唸りはじめる。引当をやり直した今日のリストは、棚と合っている。
「現場じゃ、これをポカヨケって呼ぶんですよ」と、俺は言った。「人は間違える前提で、間違えても刺さらない治具を作っとく。指差呼称は、治具が作れんところの最後の手段で。——俺は、コードで最後の手段ばっかりやってた。紙と、指差しで」
「治具が、この骨格です」と親方は言った。「これからは、間違えようとしても、骨格が刺させない」
親方は走り出したコンベアを窓から一瞥して、鞄を持った。去り際に、こっちを見ずに言った。
「同じものを二度写したら、三度目の前に束ねてください。反応でも、型でも、手順の骨でも——写しは育って、いつか、同じ直しが当たらなくなる」
反応でも、型でも、というのが何の話かは、俺には分からなかった。俺には手順の骨の話で十分だ。ただ、あの人は同じ病気を方々で診てきたんだろう、とは思った。写しが育つ場所なら、どこでも。
報酬の話になって、親方は金額の代わりに条件を言った。四本目——月次の請求連携のことまで知っていた——を作る日は、骨格を書かないこと。若手に、穴を四つ埋めさせること。骨格に手を入れる日は、先に骨格のテストを回すこと。一本だから、すぐ終わる。
俺は階段の下まで送った。現場の音の中、親方の背中はすぐ見えなくなった。フォークのうまい奴の旋回半径みたいに、無駄のない去り方だった。
整備記録簿
| こんな異音・症状が出たら | 入れるべき整備(Template Method) | まだ様子見でいい |
|---|---|---|
| 「接続→前処理→本処理→後処理→クローズ」のような手順の骨格ごとコピペしたコードが複数あり、共通部の修正をコピーの数だけ配って回っている | ✓ | |
| コピーどうしの形が少しずつずれて(変数名・関数の切り方・ログ)、同じ修正が同じ形で当たらない | ✓ | |
| 失敗時の挙動(止まる/止まらない・通知の有無・終了コード)がコピーごとにバラバラで、どれが正かもう分からない | ✓ | |
| まだ1本しか無い | ✓(2本目を書く日に骨格を考えれば間に合う) | |
| 手順の章立て自体が別物 | ✓(共通でないものを無理に一本へ束ねるほうが事故) |
整備手順
- 似たコードを並べて読み、共通の行に線を引く。線は字面ではなく意味で引く(
cとconnは同じ接続)。線が引けた行=骨格、引けなかった行=機械ごとの工程(穴)と仕分ける。 - 骨格を関数1本(
func Run(name string, s Steps) error)に書き起こす。順序・リトライ・通知・defer Close・「失敗は必ず error で返す」を骨格に持たせる。終了コードを決めるのはmainの仕事にして、Runは事実(error)だけを返す。 - 穴を interface(
type Steps interface { Connect / Prepare / Process / Finish })として宣言する。穴は増やしすぎない——穴が増えるほど差し込みページ1枚に書く義務が増え、「四つ埋めれば動く」の手軽さが崩れる。迷ったら工程だけにして、名前や設定はRunの引数へ。 - コピーを1本ずつ、Steps 実装(差し込みページ)に書き直す。骨格由来の行——リトライ・通知・クローズ——がゼロになるまで削る。
- 正常系の結果が書き直し前と変わらないことをテストで確かめる。整備で挙動を変えない。変えてよいのは「失敗したときに正直に赤になる」ことだけ。
- 次の共通改修からは骨格を1回直すだけ。新しいバッチは差し込みページを1枚書くだけ——骨格は写さない。
親方より
動いている物を写すのは、現場じゃ堅実のうちだ。だから写したこと自体は、責めない。問題は写したあとだ——写しは育つ。変数の名が変わり、関数の切り方が変わり、ログの文句が変わる。そしてある日、共通の直しが出たとき、同じ直しが、同じ形で当たらなくなっている。配り漏れた一枚が、黙って緑のまま、嘘をつく。
同じものを二度写したら、三度目の前に束ねろ。手順の骨は一本にして、機械ごとに違う工程だけを差し込め。直しは原本に一回。信号は骨格が握る。——そうしておけば、お前の指差しは、また仕事をする。
