同じ役職名を6箇所で組み立てていた唯一の生成点に集約する
決裁欄の「◯◯課長」を作るロジックが帳票・画面・マトリクスに6重複し、画面ごとに表示がズレていた。唯一の生成関数へ集約するまでと、差分ゼロを確認してから切り替える移行のポイント。
現象:同じ職員なのに画面で役職名が違う
決裁の書類には、承認者の役職を「◯◯課長」「◯◯係長」のように表示する。所属名と役職を組み合わせた、ただの文字列だ。ところがある日、同じ職員の役職名が、印刷した帳票PDFとオンラインの決裁状況画面とで食い違っている、という指摘が来た。
原因を追うと、この「役職の表示名を組み立てる処理」がコードのあちこちに散らばっていた。帳票PDF、オンライン決裁状況、決裁マトリクス、確認用のリスト……数えると6箇所。どれも似たような switch で「課長 / 係長 / それ以外」を分岐し、似たような str_replace で表記を整えていた。似ているが、微妙に違う。だから画面によって結果がズレる。
厄介なのは表記調整だ。自治体ごとに「係」を「担当」と呼ぶ、課の前に部名を付ける、といった団体ごとの特例がある。その str_replace が6箇所に別々にコピーされていた。ひとつ直しても、残り5箇所は古いまま。直したそばから別の場所で再発する。典型的な保守地獄である。
なぜ重複したか
最初は悪意なく始まる。帳票を作るときに役職名が要る。手近な場所に switch を書く。次にオンライン画面を作るとき、帳票のコードをコピーして少し直す。マトリクスでも同じことをする。一回ごとの判断は合理的だ。「役職名を作る」という同じ判断が、6回、別々の場所で下されただけである。
だが同じ判断を複数箇所に持つと、それらは必ずいつか食い違う。片方だけ直されるからだ。表示のズレはバグではなく、この構造の必然的な帰結だった。直すべきは個々の str_replace ではなく、「判断が1つなのに実装が6つある」という状態そのものだった。
対策:生成点を1つにする
やったことは単純で、役職名の生成を「唯一の関数」に集約した。ただし責務を2段に分けた。
- 1段目:課長 / 係長 / その他の分岐。役職コードから基本の表示名を決める。
- 2段目:表記ゆれと団体特例の正規化。「係→担当」のような
str_replaceはここだけに置く。
// 呼び出し元はこれ1つを通る。分岐も置換もこの内側にしかない
function buildPostName(int $postCode, string $divName): string
{
$base = formatBasePost($postCode, $divName); // 課長/係長/その他の分岐
return normalizePostName($base); // 表記ゆれ・団体特例の吸収
}
肝は「全ての呼び出し元がここを通る」ことだ。帳票もオンラインもマトリクスも、自前の switch を捨て、この関数を呼ぶだけにする。判断が1箇所に集まれば、団体特例の追加は normalizePostName の1行で済み、全画面に同時に効く。
移行のポイント:差分ゼロを確認してから切り替える
集約そのものより、移行のほうが怖い。6箇所には長年の細かな調整が埋もれていて、いきなり新関数へ差し替えれば、意図した挙動まで壊しかねない。
そこで、まず「正しい表示」の基準を決めた。監査用の確認リスト、つまり人が見て正しいと合意できる表示を真値とする。次に、新関数の出力と旧コードの出力を全職員ぶん横並びで出し、差分を機械的に洗い出した。
一致する行は放っておく。相違行だけを1つずつ検証する。ある課では旧503件・新503件が全一致、別の課では504件中1件だけ違い、調べると相違は旧側のバグ(古い str_replace の消し漏れ)だった、という具合だ。こうして「新旧の違い=旧側の既知バグのみ」まで詰め、差分がゼロ(あるいは説明のつく差だけ)になったところで初めて呼び出し元を切り替えた。
新旧を突き合わせるこの一手間が、リファクタを「動作を変えない置き換え」に留める安全弁になる。
AIとの協働作業での学び
Single Source of Truth。同じ判断が複数箇所に重複したら、表示のズレはいつか必ず起きる。片方だけが直されるからだ。だから重複を見つけたら、個々の症状を潰すより、判断の生成点を1つに寄せる。
そして集約する時は、新旧の出力を突き合わせ、差分がゼロになったことを確認してから切り替える。「たぶん同じ」ではなく「並べて確かめた」を通ってこそ、リファクタは安全になる。