プライベート銀行口座、プライベート現金、事業用銀行口座、事業用現金、取引相手、個人クレジットカード、事業用クレジットカードの7種類のToDoを、すべてTodoHandlerから専用Livewireコンポーネントへ書き直した。定期収入・定期支出の2つはまだTodoHandlerのまま残っていて、これは後日やる。
きっかけは「別画面でも同じ登録をしたい」だった
ToDo駆動オンボーディングの再設計のときにTodoHandlerという統一インターフェースを作った。inputSchemaでフォームの形を宣言し、validateで検証し、executeでドメインモデルに書き込む、という形にしておけば、新しいToDoを追加するたびに同じ手順で済むという狙いだった。
きっかけは、銀行口座の登録を「入出金の場所」一覧ページなど、ToDo経由ではない別の画面からも呼びたくなったことだった。ここでTodoHandlerという抽象がToDoの存在を前提にしすぎていて、Todoを経由しない場所からは呼びにくいことが分かった。フォームの入力スキーマも検証もTodoHandlerの中に押し込めていたので、「Todoのカードとして表示する」以外の使い方をしようとすると、結局Handlerの中身をほぼ全部剥がして書き直すことになる。
各Todoを専用のLivewireコンポーネントに戻した
結局、7種類のToDoすべてで同じ作業をした。TodoHandler実装を削除し、代わりに専用のFormクラスと専用のLivewireコンポーネント・Bladeビューを新設する。ToDoカードからも、ToDoを経由しない他の画面からも、同じLivewireコンポーネントを呼べば同じ登録処理になる。1件あたりの差分はだいたい700〜900行の増減で、7件合計すると増減あわせて1万行近く触ることになった。
最初の判断は良かったが、順番を間違えた
TodoHandlerという統一インターフェースを最初に決めたこと自体は今でも悪くない判断だったと思っている。実際、銀行口座・現金・取引先・定期取引・期首仕訳と、最初の数種類は同じ型に載せることでスムーズに追加できた。
問題は順番だった。「まず個別の機能(画面)を作り、それを後からToDoに拡張する」方が良かった。今回は逆に、最初からToDo前提でHandlerを設計してしまったので、「Todoを経由しない単体の登録画面」という要求が出てきた瞬間に、Todo向けに最適化された抽象がそのまま重荷になった。個別機能が先にあれば、ToDo用のラッパーを薄く被せるだけで済んだはずで、両方向に使える形にするコストはこんなに高くならなかったはずだ。
しかも今回は対象が1つだけでなく7つもあったので、同じ書き直しパターンを7回繰り返す羽目になり、素直に大変だった。ToDoHandlerという「早すぎる統一」に飛びついたツケを、7倍にして払った格好になる。
Codex 5.4 miniとClaude Codeを往復させて乗り切った
7回分の書き直しをまともにやると生成AIのクレジットをかなり消費する。そこで、実装はCodex 5.4 miniに任せ、Claude Codeにはレビューだけをさせる役割分担にした。
流れは単純で、Codex 5.4 miniで1件分の書き直しを実装し、Claude Codeにレビューさせる。指摘が出たら再びCodex 5.4 miniに戻して修正させ、またClaude Codeでレビューする。これをテストがGreenになり指摘もなくなるまで繰り返してから、最後に人間がレビューする。実装の粒度でモデルを固定せず、実装用と査読用でモデルを分けて往復させることで、1モデルに全部通しでやらせるより安く安定して7件をさばけた。同じパターンの繰り返し作業だったからこそ、この往復がはまった。
次に似た匂いを感じたら
複数の呼び出し元から使われる可能性がある機能は、まず一番具体的な1つの使い方(今回なら「Todoカードからの登録」)に特化させず、単体で完結する形を先に作ってから、必要な文脈(ToDo、単体ページなど)ごとに薄いアダプターを被せる。共通インターフェースは、複数の実装が出揃ってから抽出する方が安全、というのを[actorガードの記事]とはまた別の角度で学んだ。
定期収入・定期支出の2つが片付いたら、TodoHandlerはこの設計から完全に退場することになる。