Featured image of post コードメカニック【Template Method】同じ直しが、三本目にだけ当たらなかった〜丸写しの骨格を、一本の手順書に組み直す〜

コードメカニック【Template Method】同じ直しが、三本目にだけ当たらなかった〜丸写しの骨格を、一本の手順書に組み直す〜

夜間バッチ3本が骨格ごとコピペで動いていた。共通のエラー処理改修が一本だけ当てられず、接続断の朝、そのバッチだけ黙って正常終了——在庫が昨日のまま荷主に渡る。骨格を一本に固定し工程だけ差し込むTemplate Methodの整備記録。

数が合わない朝

内線が鳴ったのは、朝の八時十二分だった。

「2階。Aの13番、リストと棚が合わん。リストは40って言うんだが、棚には28しか無いぞ」

現場の班長だ。あの人は数えで間違えたことがない。検品二十年、俺がフォークに乗っていた頃からの付き合いだ。だから俺は、現場を疑う順番を最初から飛ばした。数えなら現場が正しい。なら、嘘をついているのはうちの2階——俺の機械のほうだ。

俺は地場の3PL——荷主の物流をまるごと請け負う倉庫会社の社内SEをやっている。五十四。元は現場で、入出荷を二十年。腰を痛めて事務所に上がってから、Excelのマクロ、Access、と独学でここまで来た。情シスは実質、俺ひとりだ。

調べているうちに、口の中が乾いてきた。荷主の基幹システムは、昨日の在庫で今日の引当——注文へ在庫を割り当てる処理——をかけていた。うちのWMS(倉庫管理システム)から毎晩3時に送る在庫スナップショット——全SKU(品目ごとの管理単位)の現在庫を書き出して荷主へ渡すファイルが、昨夜は届いていない。荷主の営業は、もう存在しない28個を、今朝、売ったことになる。

スケジューラの実行結果一覧を開く。深夜のバッチは3本。仕入先からの入荷実績取込。荷主基幹への出荷実績連携。そして在庫スナップショット。

全部、緑だった。

念のために言っておくと、この緑と赤は、プログラムが終わるときに残す番号——終了コードで決まる。0なら成功で緑、それ以外は失敗で赤。画面は処理の中身を見ていない。番号だけを見ている。

そして俺は思い出していた。夜中の2時、出荷実績連携の「接続失敗・異常終了」の通知で一度起こされたのだ。荷主側の基幹メンテが延びて、接続が切れていた。メンテ明けの5時に手で流し直して、画面が緑に変わるのを確認して——指差して、ヨシ、とまで言って——二度寝した。

在庫スナップショットは3時に動いたことになっている。緑で。

ログを開いた。書いてあった。

1
connect wms-edge: timeout

ログには、書いてあったんだ。誰も毎朝ログは読まない。読むのは画面の色だ。

緑だった。俺は確認した。確認は正しかった。緑が嘘だった。

荷主の情シス担当に電話して、頭を下げた。引当を止めてもらい、再送の段取りを決める。3PLは数字の信用で飯を食っている。誤出荷の一件より、昨日の数字を今日の数字として渡していたことのほうが、商売の根に刺さる。

電話の終わりに、先方が言った。

「原因の切り分け、手が要るようでしたら——うちが前に世話になった人がいますよ。出張整備の。コードのほうの、ですけど。みんな“親方”って呼んでました」

コードの、整備。聞いたことのない商売だった。だが荷主筋の紹介だ。断る選択肢は、最初から無い。親方、という呼び名だけが妙に腑に落ちた。現場で年長の職人をそう呼ぶ。俺の口にも、馴染みのある言葉だ。

親方が来たのは、昼前だった。

ラフな格好に薄い鞄。女の人だ、というのが最初の感想で、それ以上の感想を持つ前に、名刺より先に口が動いた。「止まっていたのは、どの子です」

機械を「子」と呼ぶ言い方に、おや、と思った。現場の年寄りがやるやつだ。

「……止まらなかったんです。止まるべきやつが」

親方の目が、少しだけ動いたように見えた。

俺は経緯を一気に説明した。三本のバッチ。夜中のメンテ延長。出荷連携は止まって通知してきたこと。スナップショットは黙って緑だったこと。説明しながらスケジューラの画面を見せたが、親方は画面より先に、デスクの黒いバインダーに目を留めた。俺の点検表綴りだ。現場の癖が抜けなくて、機械もコードも、確認事項は紙に書いて指差すことにしている。

「先に、その紙を見せてもらえますか」

めくる手が、途中で二往復した。それから返してよこして、一言。

「これは、続けてください」

褒められたのか、指示されたのか分からなかったが、悪い気はしなかった。だが紙は今朝、役に立たなかった。

コードを見せた。まず、夜中にちゃんと止まって通知してきた側——出荷実績連携だ。

 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
// shipment.go — 出荷実績連携バッチ。毎晩2時に動く。
// 半年前の改修済み: 接続は3回まで試す。ダメなら通知して異常終了。
func main() {
	if err := run(); err != nil {
		log.Fatal(err) // 非ゼロ終了 → 実行結果一覧が赤になる
	}
}

func run() error {
	// --- 接続(半年前の改修ブロック) ---
	var conn io.Closer // ループの外でも使うため、先に宣言しておく
	var err error
	for i := 0; i < 3; i++ {
		conn, err = dialShipment()
		if err == nil {
			break
		}
		time.Sleep(retryWait)
	}
	if err != nil {
		notifyOps("出荷実績連携", "接続", err)
		return err
	}
	defer conn.Close() // defer = この関数を抜けるとき必ず実行する予約

	// --- 前処理: 未連携の出荷実績を抽出 ---
	recs, err := fetchUnsentShipments(conn)
	if err != nil {
		notifyOps("出荷実績連携", "前処理", err)
		return err
	}

	// --- 本処理: 荷主基幹へ送信 ---
	for _, r := range recs {
		if err := sendShipment(conn, r); err != nil {
			notifyOps("出荷実績連携", "本処理", err)
			return err
		}
	}

	// --- 後処理: 連携済みマーク ---
	if err := markShipmentSent(conn, recs); err != nil {
		notifyOps("出荷実績連携", "後処理", err)
		return err
	}
	log.Printf("出荷実績連携: %d件 完了", len(recs))
	return nil
}

半年前、接続断で入荷取込が黙って空振りする小さい事故があった。それを機に「接続失敗は3回まで試す。ダメなら担当へ通知して、異常終了する」という改修を決めて、入荷と出荷の2本には入れた。log.Fatal は、エラーを記録してプログラムを失敗の番号——非ゼロの終了コードで終わらせる。だから画面が赤になり、通知が飛び、俺は夜中に起こされる。起こされるのは、正しい設計だ。

「入荷取込も、ほぼこの写しです。半年前の直しも入ってます。——問題は、三本目で」

見てほしいのは2点だ。形がところどころ違うこと。そして、さっきの「改修ブロック」が無いこと。

 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
// snapshot.go — 在庫スナップショットバッチ。毎晩3時に動く。
// 2年前、荷主の要望で急ぎ追加。出荷連携を写して作った——はずだった。
func main() {
	// 接続。落ちてもバッチは止めない方針(当時の俺のメモ: 朝までに直せばいい)
	c, err := dialWMS()
	if err != nil {
		log.Printf("connect wms-edge: %v", err) // 書くだけ。止まらない
		return // そのまま正常終了。画面は緑
	}
	defer c.Close()

	// 前処理と本処理がべた書き(急いでいたので関数に切っていない)
	rows, err := queryStock(c)
	if err != nil {
		log.Printf("query: %v", err)
		return
	}
	f, err := os.Create(snapshotPath)
	if err != nil {
		log.Printf("create: %v", err)
		return
	}
	defer f.Close()
	for _, row := range rows {
		if _, err := fmt.Fprintf(f, "%s,%d\n", row.SKU, row.Qty); err != nil {
			log.Printf("write: %v", err)
			return
		}
	}

	// 後処理: 荷主へアップロード
	if err := upload(snapshotPath); err != nil {
		log.Printf("upload: %v", err)
		return
	}
	log.Printf("snapshot ok: %d rows", len(rows))
}

エラーで止まらないんです、こいつ。全部 log.Printf して、returnmain が普通に終わるから、終了コードは0。画面は緑。半年前にそれをやめる改修をしたのに、こいつにだけ、入れてない。

「俺のポカです」

親方はそれを、肯定も否定もしなかった。「直しましょう。話はそれからだ」

直し方は、分かっている。さっきの改修ブロックを持ってくればいい。分からんのは——なんで俺は、これを半年も放っておけたのか、のほうだった。

三本目だけ、同じ直しが当たらない

親方の応急処置は、見ものだった。正確には——見ものになるはずが、ならなかったことが、見ものだった。

親方は出荷連携のファイルから、改修ブロック——リトライのループと通知とエラー返しの一塊——を選択してコピーした。スナップショットのファイルを開く。貼る。

手が、止まった。

貼り先が、無いのだ。向こうは conn、こっちは c。向こうは接続のあとに前処理の関数が並ぶが、こっちはべた書きで、行の対応が取れない。エラーの返し方も違う。向こうは err を返して mainlog.Fatal に渡すが、こっちの main は何も受け取らない作りだ。

親方は貼るのをやめて、無言で手で書き直しはじめた。まず main を、出荷連携と同じ「mainrun() を呼んで、エラーなら log.Fatal」の形に割る。そのうえで dialWMS をリトライのループで包み、通知を呼べる形に揃え、握りつぶしていた return を、エラーを返して非ゼロで終わる形へ一行ずつ直していく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
	// 応急: 半年前の改修を、三本目にも——手作業で移植した
	var c *wmsConn
	var err error
	for i := 0; i < 3; i++ {
		c, err = dialWMS()
		if err == nil {
			break
		}
		time.Sleep(retryWait)
	}
	if err != nil {
		notifyOps("在庫スナップショット", "接続", err)
		return err // もう黙らない。画面は赤になる
	}
	defer c.Close()

残りの log.Printf して握りつぶす行も、同じ要領で、一箇所ずつエラーを返す形へ。コピーで済むはずの作業を、形を合わせながら、手で。五分かかった。

その五分の間、俺はずっと親方の手元を見ていた。迷いがない。行く先を全部決めてから指が動いている。フォークのうまい奴の旋回と同じだ。——これは銭の取れる仕事だな、と職人の値踏みで思った。

書き終えて、親方が初めて口を開いた。

「同じ意味の直しを、いま、三つ目の言い方で書きました。一本目と二本目は、写しが揃っていたから同じ形で済んだ。三本目は——揃っていない。だから、当たらない」

直したスナップショットを手で流す。今日の在庫が荷主へ飛ぶ。先方の情シスから「届きました。引当やり直します」と電話が来て、昼過ぎには現場のリストの数が棚と合った。

「今日の取りこぼしは、これで終わりです。——ですが、これで三回目です」

「三回目?」

「同じ意味の直しを、あなたが半年前に二回——入荷と出荷に——書いた。いま私が、三回目を書いた。四本目のバッチができたら、誰かが四回目を書きますか」

四本目は、作る。たぶん。荷主からは月次の請求データ連携の話がもう来ている。そのたびに、また写して、また直しを配って回るのか。

「……配るたびに、紙のリストの行が増えるだけじゃないですか」

口に出してから、自分の言葉に引っかかった。紙のリスト。そうだ、俺は半年前、この配り物のために紙を作ったはずだ。

保留、と俺の字で書いてあった

「さっきの紙を、もう一度」と親方が言った。「半年前の改修のページを」

バインダーの主は日々の点検表で、半年前のページは分厚い綴りの奥に埋もれていた。めくる。「障害対応改修 配布チェック」。俺が作った配布管理のページだ。

入荷実績取込——✓、済。 出荷実績連携——✓、済。 三行目。在庫スナップショット——

「保留(構成が違うため要調査)」。

俺の字で、書いてあった。

忘れていたのは、直すことじゃなかった。保留した理由のほうだ。「構成が違うため要調査」——半年の間に、俺はこの理由を「俺のポカ」という言葉で上書きしていた。そのほうが、形の違うあいつを開けて読み解くより、楽だったからだ。

「あなたは忘れたんじゃない」と親方が言った。「当てられなかったんです。さっき私が当てて見せたでしょう——五分かかった。揃っていれば貼って終わりの直しが、です。あなたのポカじゃない。写しが育って、同じ直しが同じ形で当たらなくなっていた。それが原因です」

慰めの口調ではなかった。事実を読み上げる声だった。だから、効いた。

「見ます」と親方が言って、エディタの画面を三つに割った。入荷、出荷、スナップショット。三本並べて、上から読みながら、同じ意味の行に線を引いていく。接続。前処理。本処理。後処理。クローズ。入荷と出荷は、線が同じ高さに揃って並ぶ。スナップショットだけ、引く場所が上下に散って、線が斜めに乱れた。

「線、ここは乱れますよ」と俺は口を挟んだ。「字が違う。こっちは conn で、こっちは c だ」

「字面じゃなく、意味で引きます。cconn も、dialWMSdialShipment も——やっていることは同じ接続だ。意味が同じなら、同じ印」

「形は崩れていても」と親方は続けた。「読んでいくと、同じ手順です。接続、前処理、本処理、後処理、クローズ。三本とも、骨は同じだ。違うのは、骨に付いている肉のほうで」

骨、という言葉で、腑に落ちるものがあった。

「……手順書だ。うちの現場の。検品も積み込みも、手順書の章立ては同じで、機械ごとに工程の中身だけ違う。これは——同じ手順書を、機械ごとに丸写しして、三冊持ってる状態だ」

「そう。そして半年前、あなたは三冊全部に同じ赤字を入れて回る羽目になった。二冊で力尽きた。三冊目は字が崩れていて、どこに赤を入れていいか、分からなかった」

親方は画面を指したまま、名前を口にした。

「これはコピペプログラミング——動いているコードを丸写しして増やすやり方です。一枚写すだけなら、手堅さのうちだ。だが手順の骨格ごと写すと、共通の直しが出るたびに、写しの数だけ同じ直しを配って回ることになる。配り漏れと、ドリフトが静かに積もる」

ドリフト。コピーどうしが手直しのたびに少しずつ形を変えて、もう同じ直しが同じ形で当たらなくなるほど離れていくこと——親方は、それだけ言い添えた。うちの三本目が、まさにそれだった。2年前、急ぎで写したとき、関数に切るのを省いて、変数名もログも自分流に書いた。あの日の省略が、半年前の「保留」を生み、保留が今朝の「黙って緑」になった。

写すのが悪いんじゃない。写したあとの話なんだ。写しは、生き物みたいに育つ。

整備工場で言えば、同じ設計 of 機械を三台並べて、それぞれ別々に手を入れて回るようなものだ。二台までは新型の部品を組み込めたが、構造が変わってしまっていた三台目だけは、部品が合わずに古いまま放置された。

コピペで複製され個別にドリフトした3つのバッチ機械。入荷と出荷の機械には改修プレートが装着されているが、在庫スナップショットの機械だけ改修プレートがなく配線が乱れており、エラーでも正常(緑)の偽信号を出している状態。

同じ骨格が三回書かれ、改修ブロックは二箇所にしか無い。それが今朝の、絵解きだった。

俺は画面の赤線を眺めて、訊いた。

「骨が同じなら——骨だけ、一本にできんのですか。現場の手順書なら、共通の章は一冊にまとめて、機械ごとの工程だけ差し替える。コードで、それは」

「できます」と親方は言った。「それが定石です」

骨を一本にして、工程だけ差し込む

「やることは、さっきの線の通りです」と親方は言った。「印が引けた行——三本に共通の行は、骨格として一本にまとめる。引けなかった行——機械ごとに違う中身は、穴にして、あとから差し込む」

共通の章は一冊に。機械ごとの工程は、差し込みページに。俺の手順書の言葉で言えば、そういうことだ。

親方がまず書いたのは、穴の宣言だった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package batch

// Steps は、バッチごとに違う工程だけを書く「差し込みページ」。
// 骨格(Run)のことは何も知らなくていい。工程の事実だけを返す。
type Steps interface {
	Connect() (io.Closer, error) // どこに繋ぐか
	Prepare() error              // 前処理
	Process() error              // 本処理
	Finish() error               // 後処理
}

interface というのは、必要なメソッドの一覧だけを決めた約束事だ。ここでは「繋ぐ・整える・本番・後始末」の四つ。この四つを持っていれば、誰でも差し込みページになれる。

約束事の実例は、すぐ隣にもある。Connect が返す io.Closer も標準ライブラリの interface で、中身は「Close() できるもの」という、いちばん小さい約束事だ。

なお、この batch パッケージは共通部品として一本だけ置く。三本のバッチは、それぞれの main から import して使う。

そして骨格——手順書の本体。

 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
// Run が手順書の本体。順番と、しくじったときの段取りは、ここが握る。
func Run(name string, s Steps) error {
	// --- 接続。3回まで試す(半年前の改修は、今日からここに一回だけ書いてある) ---
	var conn io.Closer
	var err error
	for i := 0; i < 3; i++ {
		conn, err = s.Connect()
		if err == nil {
			break
		}
		time.Sleep(retryWait)
	}
	if err != nil {
		notifyOps(name, "接続", err)
		return err // 必ず失敗で返す。黙って緑にはならない
	}
	defer conn.Close() // クローズは骨格が保証する

	if err := s.Prepare(); err != nil {
		notifyOps(name, "前処理", err)
		return err
	}
	if err := s.Process(); err != nil {
		notifyOps(name, "本処理", err)
		return err
	}
	if err := s.Finish(); err != nil {
		notifyOps(name, "後処理", err)
		return err
	}
	log.Printf("%s: 完了", name)
	return nil
}

defer は「この関数を抜けるとき、必ず最後に実行する」という予約だ。途中のどの工程で失敗しても、接続のクローズだけは骨格が必ず面倒を見る。

なお、やり直すのは繋ぎ直しだけだ。前処理や本処理が落ちたとき、中身の途中からやり直すことはしない。途中再開はまた別の設計の話で、今日の骨格はそこに踏み込まない——通知して、正直に赤になる。それだけを約束する。

これがTemplate Method(テンプレートメソッド)——手順の骨格を1か所に固定し、変わる工程だけを後から差し替えられるようにする技法だ。本来は継承——親の手順書を子がまるごと引き継ぐ仕組み——を持つ言語の定石だが、Goに継承は無い。だから、差し替えたい工程のほうを interface にして、骨格に渡す。骨格が固定で、工程が可変。向きはどの言語でも同じだ。

俺は Run の中身を上から読んで、引っかかった。

「この 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
// 在庫スナップショットの「差し込みページ」。骨格のことは何も知らない。
type snapshotSteps struct {
	conn *wmsConn
	rows []StockRow
	path string
}

func (s *snapshotSteps) Connect() (io.Closer, error) {
	c, err := dialWMS()
	if err != nil {
		return nil, err // 止めるかどうかは骨格が決める。ここは事実だけ返す
	}
	s.conn = c
	return c, nil
}

func (s *snapshotSteps) Prepare() error {
	rows, err := queryStock(s.conn)
	if err != nil {
		return err
	}
	s.rows = rows
	return nil
}

func (s *snapshotSteps) Process() error {
	var content string
	for _, row := range s.rows {
		content += fmt.Sprintf("%s,%d\n", row.SKU, row.Qty)
	}
	s.path = snapshotPath
	// []byte に変換して書き出す。0644 はファイルの読み書き権限
	return os.WriteFile(s.path, []byte(content), 0644)
}

func (s *snapshotSteps) Finish() error {
	return upload(s.path)
}

func main() {
	if err := batch.Run("在庫スナップショット", &snapshotSteps{}); err != nil {
		os.Exit(1) // 失敗は必ず非ゼロ。画面は赤になる
	}
}

書き方も、二つだけ覚えれば読める。func (s *snapshotSteps) Connect() の、関数名の前の括弧——これは「この関数は snapshotSteps に紐づく」という Go の流儀で、いわゆるメソッドだ。そして main&snapshotSteps{} は、その実体をひとつ作って骨格に渡している。

Connect が接続を「返し」つつ、自分でも「持つ」のが目を引いた。訊くと、返すのは閉じる係——骨格の defer Close へ渡す分、持つのは自分の前処理で使う分だという。役割で分けてある。

ちなみに Go では、snapshotStepsSteps を満たすのに宣言は要らない。四つのメソッドが揃っていれば、それで Steps だ。implements と書く場所は無い。

入荷も出荷も、同じ要領で差し込みページになった。それぞれのファイルから、リトライのループが消え、通知が消え、defer Close が消えた。残ったのは、そのバッチにしか無い工程だけ。

俺はもう一度、スナップショットの新しいファイルを上から読んだ。読んでから、確かめるように訊いた。

「リトライも通知も、こっちのファイルには、もう書いてない。……次にまた共通の直しが出たら、どうなるんです」

Run を一回直す。三本とも、その場で直り終わる。配って回る紙のリストは、要らない。——半年前のあなたの改修は、この形なら一行の仕事だった」

三冊に赤字を配るんじゃなく、原本を直せば、差し込みページは全部そのまま。保留する余地が、そもそも無い。

それでも俺の中の現場の人間が、最後の確認をした。

「けど、これ、お行儀のいい設計ってやつでしょう。現場のコードで、ほんとにやるんですか」

親方は答える代わりに、小さなコードを見せた。

1
2
3
4
5
6
7
type ByAge []Person

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }

sort.Sort(ByAge(people)) // 年齢順に並ぶ

「Goの標準ライブラリが、まさにこれです。sort.Sort——並べ替えのループ、つまり骨格は、sort が一本だけ持っている。使う側が書くのは LenLessSwap の三つの穴だけ。——ソートのループを、あなたは書いたことが無いでしょう。でも、並ぶ」

言われてみれば、書いたことが無い。並べ替えなんて、呼べば並ぶものだと思っていた。骨は最初から、向こうが握っていたのか。

「工程を差し替えるってのは、その」と、もうひとつ気になっていたことを訊いた。「処理を丸ごと取っ替えるのとは、違うんですか」

「丸ごと差し替えるのは Strategy——処理のやり方を、後から丸ごと差し替えられるようにする技法。あれは呼ぶ側が手順を握ったまま、中身を取っ替える。今日のは逆です。手順は骨格が握って離さない。差し替えるのは、枠の中の工程だけ。主導権の場所が違う」

Strategy は工具の差し替え。あれでいくなら、三本の run() は三本のまま残って、中の道具だけ替わる。今日のは run() そのものを一本にした——手順書そのものが現場を仕切っている。呼ばれるのを待つのは、俺の書く差し込みページのほうだ。

「もうひとつ」と親方が言った。「失敗の出口が Run に一本化されたので、どのバッチも、しくじれば必ず赤になります。黙って緑、は構造上できなくなった」

それは、今朝の俺に一番効く話だった。

「……今朝の俺の指差しは、間違ってなかったことになるんですな。信号が正直なら、指差しは仕事をする。嘘の緑を指差して、ヨシって言ってたのが、今朝までの俺で」

「指差しをやめる必要はない。指差しに値する信号にする——それが整備です」

その整備の図面が、頭の中に浮かんだ。 頑丈な制御盤が中央に一台据え付けられ、そこから伸びる接続口に、それぞれの処理に必要なアタッチメントを差し込むイメージだ。

Template Methodにより一本化されたバッチ骨格制御盤と、3つの差し込み式アタッチメントプレート。在庫スナップショットも独立したアタッチメントになり、エラー時には制御盤が正しく赤信号を出す構造。

共通の直しは Run の一箇所。バッチを足すときは、差し込みページを一枚書くだけ。

最後に、ずっと胸にあったことを訊いた。

「これ、若手にも書けるようになりますか。俺が、いつまでもいるわけじゃないんで」

「穴は四つだけです。繋ぐ・整える・本番・後始末。骨格を知らなくても、四つ埋めれば動く。——埋めさせてみればいい」

ヨシ、が信じられる緑になる

試運転は、テストでやった。

骨格のテストは、偽物の差し込みページ——呼ばれた順番を記録するだけの Steps——を Run に渡して、約束ごとを一つずつ確かめる。接続、前処理、本処理、後処理、クローズの順で呼ばれること。接続に失敗を仕込めば3回まで試して、通知が一回飛んで、必ず失敗で返ること。途中の工程でしくじっても、クローズだけは必ず走ること。

等価性のテストも書いた。正常な晩なら、三本のバッチが生む結果——送る件数も、スナップショットの中身も——は、整備の前と後で一致する。整備で挙動を変えていないことの、文書化だ。

そして回帰テスト——同じ不具合が再発しないことを、同じ条件を再現して確かめるテストだ。今朝と同じ接続断の条件で、前のスナップショットは黙って成功し、新しいスナップショットは失敗を返す。同じ条件、違う色。なお、前の側のテストが「通る」のは変な話に聞こえるが、あれは「接続断でも緑になる」という現状をそのまま表明として書き留めたもの——今朝の事故の、文書化だ。

1
2
3
$ go test ./before ./after
ok  	code-mechanic/copy-paste-template-method/before	0.218s
ok  	code-mechanic/copy-paste-template-method/after	0.367s

「骨格のテストは、この一本きりです」と親方は言った。「バッチが何本増えても、増えるのは差し込みページと、そのテストだけ」

俺は最後に、バインダーを開いた。「障害対応改修 配布チェック」のページ——入荷✓、出荷✓、そして保留——を外して、新しい一枚を綴じた。

骨格 Run: 原本(直しはここへ一回)。 差し込み: 入荷・出荷・在庫。

三冊の手順書が、原本一冊と差し込み三枚になった。紙が、薄くなった。紙が薄くなる整備はいい整備だと、現場で二十年、そう覚えてきた。

階下から、出荷再開の構内放送が聞こえた。コンベアのモーターが唸りはじめる。引当をやり直した今日のリストは、棚と合っている。

「現場じゃ、これをポカヨケって呼ぶんですよ」と、俺は言った。「人は間違える前提で、間違えても刺さらない治具を作っとく。指差呼称は、治具が作れんところの最後の手段で。——俺は、コードで最後の手段ばっかりやってた。紙と、指差しで」

「治具が、この骨格です」と親方は言った。「これからは、間違えようとしても、骨格が刺させない」

親方は走り出したコンベアを窓から一瞥して、鞄を持った。去り際に、こっちを見ずに言った。

「同じものを二度写したら、三度目の前に束ねてください。反応でも、型でも、手順の骨でも——写しは育って、いつか、同じ直しが当たらなくなる」

反応でも、型でも、というのが何の話かは、俺には分からなかった。俺には手順の骨の話で十分だ。ただ、あの人は同じ病気を方々で診てきたんだろう、とは思った。写しが育つ場所なら、どこでも。

報酬の話になって、親方は金額の代わりに条件を言った。四本目——月次の請求連携のことまで知っていた——を作る日は、骨格を書かないこと。若手に、穴を四つ埋めさせること。骨格に手を入れる日は、先に骨格のテストを回すこと。一本だから、すぐ終わる。

俺は階段の下まで送った。現場の音の中、親方の背中はすぐ見えなくなった。フォークのうまい奴の旋回半径みたいに、無駄のない去り方だった。


整備記録簿

こんな異音・症状が出たら入れるべき整備(Template Method)まだ様子見でいい
「接続→前処理→本処理→後処理→クローズ」のような手順の骨格ごとコピペしたコードが複数あり、共通部の修正をコピーの数だけ配って回っている
コピーどうしの形が少しずつずれて(変数名・関数の切り方・ログ)、同じ修正が同じ形で当たらない
失敗時の挙動(止まる/止まらない・通知の有無・終了コード)がコピーごとにバラバラで、どれが正かもう分からない
まだ1本しか無い✓(2本目を書く日に骨格を考えれば間に合う)
手順の章立て自体が別物✓(共通でないものを無理に一本へ束ねるほうが事故)

整備手順

  1. 似たコードを並べて読み、共通の行に線を引く。線は字面ではなく意味で引く(cconn は同じ接続)。線が引けた行=骨格、引けなかった行=機械ごとの工程(穴)と仕分ける。
  2. 骨格を関数1本(func Run(name string, s Steps) error)に書き起こす。順序・リトライ・通知・defer Close・「失敗は必ず error で返す」を骨格に持たせる。終了コードを決めるのは main の仕事にして、Run は事実(error)だけを返す。
  3. 穴を interface(type Steps interface { Connect / Prepare / Process / Finish })として宣言する。穴は増やしすぎない——穴が増えるほど差し込みページ1枚に書く義務が増え、「四つ埋めれば動く」の手軽さが崩れる。迷ったら工程だけにして、名前や設定は Run の引数へ。
  4. コピーを1本ずつ、Steps 実装(差し込みページ)に書き直す。骨格由来の行——リトライ・通知・クローズ——がゼロになるまで削る。
  5. 正常系の結果が書き直し前と変わらないことをテストで確かめる。整備で挙動を変えない。変えてよいのは「失敗したときに正直に赤になる」ことだけ。
  6. 次の共通改修からは骨格を1回直すだけ。新しいバッチは差し込みページを1枚書くだけ——骨格は写さない。

親方より

動いている物を写すのは、現場じゃ堅実のうちだ。だから写したこと自体は、責めない。問題は写したあとだ——写しは育つ。変数の名が変わり、関数の切り方が変わり、ログの文句が変わる。そしてある日、共通の直しが出たとき、同じ直しが、同じ形で当たらなくなっている。配り漏れた一枚が、黙って緑のまま、嘘をつく。

同じものを二度写したら、三度目の前に束ねろ。手順の骨は一本にして、機械ごとに違う工程だけを差し込め。直しは原本に一回。信号は骨格が握る。——そうしておけば、お前の指差しは、また仕事をする。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。