プロトタイプをリスク最小でモジュール化するリファクタリング
ルート、サービス、DB、UI を段階的に整理し、各変更を小さくテスト可能でロールバックしやすく保つことで、プロトタイプを安全にモジュールへ移行する方法。

なぜプロトタイプは変更が危険になるのか
プロトタイプはすべてが近くにあるため速く感じられます。ルートがDBに触れ、レスポンスを整形し、UIがそれを描画します。その速さは本物ですが、代償を隠します: 機能が増えると、最初の「簡単な経路」がすべてが依存する経路になってしまいます。
最初に壊れるのはたいてい新しいコードではありません。古い前提です。
ルートに対する小さな変更が静かにレスポンス形状を変え、二つの画面を壊すことがあります。三箇所にコピーされた「一時的」なクエリが微妙に異なるデータを返し、どれが正しいか誰も分からなくなることもあります。
これが、大きな書き換えが善意でも失敗する理由です。構造と挙動を同時に変えると、バグが出たときに原因が新設計なのか基本的なミスなのか判別できません。信頼は下がり、スコープは膨らみ、書き換えは長引きます。
リスクの低いリファクタは、変更を小さく可逆に保つことです。どのステップでも止めて、動作するアプリが残るべきです。実践的なルールはシンプルです:
- 変更ごとに明確な1つの目的(コードを移す、動作を変えない)
- 同じユーザーフローを再テストできる短いサイクル
- すばやく実行できる退屈なロールバック計画
- 新しいコードは実証されるまで古いコードの隣に置く
ルート、サービス、DBアクセス、UIは、各層が他の層の仕事をし始めると絡み合います。ほどくことは「完璧なアーキテクチャ」を追うことではありません。一度に一本の糸を動かすことです。
コードに触る前にルールを決める
リファクタは改装ではなく引っ越しのように扱ってください。挙動は同じに保ち、後で構造を変えやすくすること。再編の間に機能を「改善」すると、何が壊れたか分からなくなります。
まだ変えないものを紙に書いてください。一般的な「まだやらない」項目: 新機能、UI再設計、DBスキーマ変更、パフォーマンス作業です。この境界がリスクを低く保ちます。
「ゴールデンパス」になるユーザーフローを一つ選んで守ってください。人が日常的に行うものを選びます。例:
sign in -> create item -> view list -> edit item -> save
このフローを各小さなステップの後に再実行します。同じ振る舞いなら作業を続けてよいでしょう。
最初のコミットの前にロールバック方法に合意してください。ロールバックは退屈であるべきです: git revert、短命の機能フラグ、あるいは復元できるプラットフォームのスナップショット。Koder.ai を使っているなら、スナップショットとロールバックが整理しながらの安全網になります。
各ステージごとに小さな完了定義を持ってください。大きなチェックリストは不要で、ただ「移動+変更」が忍び込むのを防ぐ程度で十分です:
- 同じ入力は同じ出力を生む(エラーや空状態も含む)
- ゴールデンパスが素早い手動テスト(または小さな自動チェック)にパスする
- 変更はステージのスコープ内に留まる(ルートのみ、またはサービスのみ)
- ロールバックを少なくとも一度検証する(実際に試す)
- どこが移動したか短いメモを残す
もしプロトタイプがルート、DBクエリ、UI整形をすべて扱う1ファイルなら、いきなり全部分割しないでください。まずはルートハンドラだけをフォルダに移し、ロジックはそのまま(コピーでも可)にします。それが安定したら、後のステージでサービスやDBアクセスを抽出します。
クイックインベントリ: ルート、画面、データパス
始める前に現状をマップしておきます。再設計ではなく、安全な小さな移動を可能にするための下準備です。
全てのルート/エンドポイントを書き出し、何をするかを一文で書きます。UIルート(ページ)もAPIルート(ハンドラ)も含めます。チャット駆動のジェネレータでコードを出力していれば同様に扱ってください: インベントリはユーザーが見るものとコードが実際に触るものを一致させます。
使いやすい軽量インベントリ例:
- ルート/エンドポイント + 目的(例: "/checkout" は支払いフォームを表示)
- それをトリガーするUI画面/コンポーネント
- 関与するビジネスルール(価格、バリデーション、権限)
- 触るDBテーブルとアクセス種別(読み取り/書き込み/トランザクション)
- コピーや繰り返し(同じバリデーションが三ファイルにコピーされている等)
各ルートについて短い「データパス」メモを書くと良いです:
UIイベント -> handler -> logic -> DB query -> response -> UI update
進めながらリスクのある箇所にタグを付けて、周辺コードを掃除する際に誤って触らないようにします:
- 支払いと請求
- 認証、セッション、ロール
- DBマイグレーションとシードスクリプト
- バックグラウンドジョブとスケジュールタスク
- トランザクション内で動作するもの
最後に、シンプルなターゲットモジュールマップを描きます。浅めに保ってください。目的地を選ぶのであって新しいシステムを構築するわけではありません:
routes/handlers, services, db (queries/repositories), ui (screens/components)
あるコードがどこに属するか説明できなければ、その部分は自信が付くまで後回しにしてよい候補です。
ステージ1: ロジックを変えずにルートを安定化する
まずルート(またはコントローラ)を境界として扱い、コードを改善する場所とは考えないでください。目標はエントリごとの振る舞いを同じに保ちながら、エンドポイントを予測可能な場所に置くことです。
users, orders, billing のような機能領域ごとに薄いモジュールを作ります。「移動しながらきれいにする」は避けてください。リネーム、ファイル再編成、ロジック書き換えを同じコミットでやると、原因追跡が難しくなります。
安全なシーケンス:
- 既存のルートハンドラを機能ごとにグループ化(内部が乱雑でも可)
- まずはワイヤリングコードだけを移す: パラメータ読み取り、基本バリデーション、信頼している既存関数を呼ぶ
- レスポンスを同一に保つ: ステータスコード、エラーメッセージ、ペイロード形状
- 各ルートに対してゴールデンパスの小さなチェックを一つ追加
具体例: POST /orders がJSON解析、フィールドチェック、合計計算、DB書き込み、返却を1ファイルでやっているなら、書き直さないでください。ハンドラを orders/routes に抽出し、古いロジックを createOrderLegacy(req) のように呼び出します。新しいルートモジュールは玄関口になり、レガシーロジックは当面そのままです。
生成コードを扱う場合でも(例えば Koder.ai で出力されたGoバックエンド)、考え方は同じです。各エンドポイントを予測可能な場所に置き、レガシーロジックをラップして共通リクエストが成功することを証明してください。
ステージ2: ビジネスロジックをサービスに引き出す
ルートはビジネスルールの良い居場所ではありません。すぐ成長し、関心事が混ざり、変更が危険に感じられます。
ユーザー向けアクションごとに1つのサービス関数を定義してください。ルートは入力を集め、サービスを呼び、レスポンスを返すだけにします。DB呼び出し、価格ルール、権限チェックはルートの外に置きます。
サービス関数は一つの仕事、明確な入力、明確な出力があると考えやすくなります。 "…もしかして…" を増やしているなら分割してください。
一般的に機能する命名パターン:
CreateOrder(input) -> orderCancelOrder(orderId, actor) -> resultGetOrderSummary(orderId) -> summary
ルールはサービス内に置き、UIではなくサービスが守るべきです。例えば「プレミアムユーザーは10件まで作成できる」というUIの無効化に頼るのではなく、そのルールをサービスで強制します。UIは親切なメッセージを表示できますが、ルールは一箇所にあります。
次に進む前に、変更を元に戻しやすくするために最低限のテストを追加してください:
- 正常系のテスト1つ(作成/更新が正しく行われる)
- エラー系のテスト1つ(必須フィールド欠如、権限拒否)
Koder.ai のようなツールで素早く生成や反復をする場合、サービスがアンカーになります。ルートとUIは進化できますが、ルールは安定してテスト可能です。
ステージ3: データベースアクセスを安全に隔離する
ルートが安定し、サービスができたら、DBが「どこにでもある」状態を止めてください。生のクエリを小さく退屈なデータアクセス層に隠します。
小さなモジュール(repository/store/queries)を作り、GetUserByEmail、ListInvoicesForAccount、SaveOrder のような明確な名前の関数だけを公開します。ここでエレガンスを追いかけないでください。各SQL文字列やORM呼び出しのための一つの明白な居場所を目指します。
このステージは構造に関することだけに厳密に留めてください。スキーマ変更、インデックス調整、または「ついでに」マイグレーションは避けてください。それらは別途計画してロールバックを用意するべきです。
トランザクションは一箇所に置く
プロトタイプ臭の出る典型はトランザクションが散らばっていることです: ある関数がトランザクションを開始し、別の関数が黙って自分用を開き、エラーハンドリングがファイルごとに異なる。
代わりに、コールバックをトランザクション内で実行する1つのエントリポイントを作り、リポジトリがトランザクションコンテキストを受け取れるようにします。
小さな移動を心がけてください:
- クエリを一度に一つずつリポジトリに移す(同じ入力/出力を保つ)
- サービスは新しいリポジトリ関数を直接呼ぶ(余分なロジックは入れない)
- トランザクションの開始/コミット/ロールバックを1つのヘルパーに集中させる
- エラーのマッピングを標準化する(例: “not found” と “unexpected error”)
例: “Create Project” がプロジェクトを挿入してデフォルト設定も挿入するなら、両方を1つのトランザクションヘルパーでラップします。途中で何か失敗したら、設定のないプロジェクトが残ることを防げます。
サービスが具体的なDBクライアントではなくインターフェイスに依存するようになると、多くの挙動を実際のDBなしでテストでき、恐怖が減ります。これがこの段階の目的です。
ステージ4: UIコンポーネントを再整理する(再設計はしない)
UIの整理は見た目を良くすることではありません。画面を予測可能にして副作用を減らすことです。
UIコードを技術的な種類ではなく機能ごとにグループ化します。機能フォルダはその画面、小さなコンポーネント、ローカルヘルパーを含めることができます。同じマークアップ(同じボタン行、カード、フォームフィールド)を見つけたら抽出しますが、マークアップとスタイルは同じに保ってください。
props は単純に保ちます。コンポーネントが必要とするものだけ(文字列、id、真偽値、コールバック)を渡してください。大きなオブジェクトを「念のため」渡すなら、小さな形に定義し直します。
UIコンポーネントからAPI呼び出しを取り出してください。サービスレイヤがあっても、UIにはフェッチロジック、リトライ、マッピングが残りがちです。機能ごと(またはAPI領域ごと)の小さなクライアントモジュールを作り、画面が使いやすいデータを受け取るようにします。
読み込みやエラー処理を画面間で一貫させます。予測可能な読み込み状態、再試行アクションがある一貫したエラーメッセージ、次のステップを説明する空状態を選んで再利用してください。
抽出ごとに触った画面を素早く目視でチェックします。主要な操作をクリックし、ページをリフレッシュし、一つのエラーケースを起こしてみてください。小さなステップは大きなUI書き換えに勝ります。
現実的な例: 1機能をエンドツーエンドでリファクタ
小さなプロトタイプを想像してください: サインイン、アイテム一覧、アイテム編集の3画面があります。動くが各ルートが認証チェック、ビジネスルール、SQL、UI状態を混ぜています。目的はこの機能だけをクリーンなモジュールに変え、元に戻せる変更にすることです。
前後でどこが移動するか
以前は “items” ロジックが散らばっているかもしれません:
server/
main.go
routes.go
handlers.go # sign in + items + random helpers
db.go # raw SQL helpers used everywhere
web/
pages/
SignIn.tsx
Items.tsx # fetch + state + form markup mixed
後では、振る舞いは同じだが境界が明確になります:
server/
routes/
items_routes.go
handlers/
items_handler.go
services/
items_service.go
repositories/
items_repo.go
web/
pages/
Items.tsx
components/
ItemForm.tsx
小さく可逆なエンドツーエンドの一手順
まずロジックを変えずにコードを移動します。関数名と戻り形を同じに保ち、ロールバックは主にファイル移動で済むようにします。
まず1つのルートをリファクタします。ハンドラはHTTPだけ扱い、ルールやSQLは扱いません:
// handlers/items_handler.go
func (h *ItemsHandler) UpdateItem(w http.ResponseWriter, r *http.Request) {
userID := h.auth.UserIDFromRequest(r) // same auth logic as before
itemID := mux.Vars(r)["id"]
var in UpdateItemInput
json.NewDecoder(r.Body).Decode(&in)
item, err := h.itemsService.UpdateItem(r.Context(), userID, itemID, in)
if err != nil { writeError(w, err); return }
writeJSON(w, item)
}
次にビジネスルールをサービスに移します。ここに「ユーザーは自分のアイテムだけ編集できる」や「名前は空にできない」といったチェックを置きます。サービスはDB作業のためにリポジトリを呼びます:
// services/items_service.go
func (s *ItemsService) UpdateItem(ctx context.Context, userID, itemID string, in UpdateItemInput) (Item, error) {
if strings.TrimSpace(in.Name) == "" { return Item{}, ErrInvalidName }
return s.repo.UpdateItem(ctx, userID, itemID, in)
}
その後、DBアクセスをリポジトリ関数に分離します:
// repositories/items_repo.go
func (r *ItemsRepo) UpdateItem(ctx context.Context, userID, itemID string, in UpdateItemInput) (Item, error) {
row := r.db.QueryRowContext(ctx,
`UPDATE items SET name=$1 WHERE id=$2 AND user_id=$3 RETURNING id, name`,
in.Name, itemID, userID,
)
var it Item
err := row.Scan(&it.ID, &it.Name)
return it, err
}
UI側ではページレイアウトを保ちながら、繰り返し出てくるフォームマークアップを新しい共通コンポーネントに抽出します。
pages/Items.tsxはフェッチとナビゲーションを維持components/ItemForm.tsxは入力フィールド、バリデーションメッセージ、送信ボタンを担当
Koder.ai(koder.ai)を使っているなら、深いリファクタ前にソースエクスポートを取ると便利で、移動がうまくいかなかったときにスナップショット/ロールバックで素早く復旧できます。
リスクを大きくする一般的な罠
最大のリスクは「移動」と「変更」を混ぜることです。ファイルを移動しつつロジックを書き換えると、騒がしい差分にバグが隠れます。移動は退屈に: 同じ関数、同じ入力、同じ出力、新しい居場所。
もう一つの罠は振る舞いを変える掃除です。変数名を変えるのは問題ないですが、概念の名前を変えるのは別です。もし status を文字列から数値に変えたら、それはコードの整理ではなくプロダクトの変更です。それは後で明確なテストと意図的なリリースで行ってください。
静かに問題を生むパターン
早い段階で将来のために大きなフォルダツリーや多重レイヤを作る誘惑がありますが、これはしばしば作業を遅くし、実際の作業箇所を見えにくくします。最小限の有用な境界から始め、次の機能が必要に迫った時に拡張してください。
また、UIが直接DBに触れたり(またはヘルパー経由で生のクエリを呼んだりする)ショートカットにも注意してください。速く感じますが、各画面が権限、データルール、エラーハンドリングを担うことになり責任が増えます。
避けるべきリスク増大要因:
- 多数のファイルに触れてロジックも変える大きなコミット
- 共有している戻り形をすべての呼び出し側にアップデートせずに変えること
- 明確な理由なしに新しいレイヤを追加すること
- エラーハンドリングがフラット化して(すべて
nullか一般的なメッセージになる)しまうこと
小さな例: ある画面が { ok: true, data } を期待しているのに新しいサービスが { data } を返してエラー時は例外を投げるようになると、アプリの半分がフレンドリーなメッセージを表示しなくなります。まず境界で古い形を保持し、呼び出し側を一つずつ移行してください。
次のステージに進む前のクイックチェック
次に進む前に主要な体験を壊していないことを証明してください。ゴールデンパス(サインイン、アイテム作成、表示、編集、削除)を各ステップで必ず実行します。整合性が小さな回帰を見つけやすくします。
各ステージ後の簡単なゴー/ノーゴーゲート:
- ゴールデンパスがエンドツーエンドでまだ動く(一般的なエラーケースも含む)
- ルート/コントローラは薄い: 入力を読み、1つのサービスを呼び、レスポンスを返す
- サービスはポータブル: ルート、バックグラウンドジョブ、UIアクションから呼べる(HTTPを知らない)
- DBアクセスは中央化され、トランザクションパターンとエラーハンドリングが一貫している
- ロールバックは早くて検証済み
もしどれかが失敗したら、その上に構築する前に止めて修正してください。小さなひびが後で大きくなります。
クイックな「元に戻せるか?」ドリル
マージ直後に5分だけかけてバックアウトできるかを確認します:
- ローカルブランチで最後のコミットを revert してアプリが起動し、ゴールデンパスが動くことを確認する
- 新しいコード経路をオフにする(機能フラグ、設定スイッチ、条件分岐)と旧挙動が残ることを確認する
- ゴールデンパスのログとDB書き込みを一度チェックして、保存挙動が変わっていないことを確認する
次の一手: 製品が成長してもモジュール化を保つ
勝利は最初の掃除ではありません。勝利は機能を追加しても形が保たれることです。完璧なアーキテクチャを追うのではなく、将来の変更が予測可能で小さく、元に戻しやすくすることです。
次のモジュールは「影響」と「リスク」に基づいて選んでください。イライラする箇所ではなく、ユーザーがよく触れる場所や振る舞いが既に理解されている箇所が良い候補です。テストやプロダクト上の答えが不明瞭な箇所は、より良い準備ができるまで後回しにします。
シンプルなリズムを保ちます: 1つのことを移す小さなPR、短いレビューサイクル、頻繁なリリース、そしてスコープが膨らんだら分割して小さい部分だけを出すというルール。
各ステージの前にロールバックポイントを設定してください: gitタグ、リリースブランチ、または動作することが分かっているビルド。Koder.ai を使っているなら、Planning Mode は意図せずに三層同時にリファクタする事故を防ぐのに役立ちます。
モジュール化アプリアーキテクチャの実用ルール: 新機能は同じ境界に従うこと。ルートは薄く、サービスはビジネスルールを所有し、DBコードは一箇所に集め、UIコンポーネントは表示に集中する。新機能がこれらのルールから外れるなら、変更が小さいうちに早めにリファクタしてください。
よくある質問
プロトタイプが「動く」状態でも、なぜ変更がリスクになるのか?
既定: リスクとして扱ってください。小さなレスポンス形状の変更でも複数の画面を壊すことがあります。
代わりに行うこと:
- まず挙動を凍結する(同じステータスコード、ペイロード形状、エラーメッセージ)
- 各小さな移動の後に「ゴールデンパス」を保護して再テストする
- 変更を元に戻せるようにする(小さなコミット、検証済みのロールバック)
- まずコードを移動し、改善は別の変更で行う
リファクタリング中に守るべき「ゴールデンパス」とは何ですか?
人が毎日行うフローで、コア層(認証、ルート、DB、UI)に触れるものを選んでください。
デフォルトの良い例:
- サインイン → アイテム作成 → 一覧表示 → アイテム編集 → 保存(必要なら削除まで)
繰り返し実行できるくらい小さくし、一般的な失敗ケース(例: 必須フィールドの欠如)も一つ加えて、エラーハンドリングの回帰を早く見つけられるようにします。
実際に機能する最も簡単なロールバックプランは何ですか?
数分で実行できるロールバックを使ってください。
実用的な選択肢:
- 最後のコミット(または小さなコミット群)をrevertする
- 一時的な機能フラグ/設定スイッチで旧コード経路を復元する
- 環境が対応していればプラットフォームのスナップショット/ロールバック
早い段階で一度実際にロールバックして検証しておくと、机上の計画で終わりません。
どの順番でリファクタすべきですか:ルート、サービス、DBアクセス、UI?
安全なデフォルトの順序は次の通りです:
- ルート/ハンドラ: エンドポイントを予測可能なモジュールに移動し、ロジックは同じにする
- サービス: ルートからビジネスルールを切り出し、アクションごとに関数を作る
- DBレイヤ: クエリをリポジトリ/ストアに移し、トランザクションを標準化する
- UI: 再利用可能コンポーネントを抽出し、スクリーンからAPI呼び出しを移す
この順序は影響範囲を減らします: 各層が次に触る前に明確な境界になります。
挙動を意図せず変えないようにリファクタするには?
「移動」と「変更」を別タスクにすること。
助けになるルール:
- 変更は一つの目的に絞る(出力を変えずにコードを移動)
- 境界でのリクエスト/レスポンス形状を同じに保つ
- 「ついでに」で作業しない(スキーマ変更、パフォーマンス改善、UI再設計など)
挙動を変える必要があるなら、後でテストと意図的なリリースを伴って行ってください。
生成されたコード(チャットツール等で作られたコード)でも安全にできますか?
はい。生成コードも他のレガシーコードと同様に扱います。
実用的な方法:
- 既存のロジックを新しい場所の薄いハンドラでラップする(例:
CreateOrderLegacy) - 同じリクエストが成功することを証明する(ゴールデンパス+一つのエラーケース)
- 安定したら、ラッパーの背後でサービスとリポジトリを抽出する
生成コードは外部挙動を一貫して保てれば安全に再構成できます。
トランザクションを壊さずにどう扱うべきですか?
トランザクションを集中化して退屈にしてください。
デフォルトのパターン:
- 1つのヘルパーがトランザクションを開始/コミット/ロールバックする
- リポジトリは自分でトランザクションを開かず、トランザクション/コンテキストを受け取る
- サービスが「いつ」トランザクションを必要とするかを決め、リポジトリはクエリを実行するだけ
これにより部分的な書き込み(設定のないプロジェクトが作られる等)を防ぎ、障害を追いやすくします。
リファクタをより安全にする最低限のテストは何ですか?
リファクタを安全にするための最低限のテストは次の通りです:
- 主要なサービスアクションごとに1つのハッピーパスのテスト(作成/更新が正しく行われる)
- 1つのエラーパスのテスト(必須フィールド欠如、権限拒否など)
- 各小さな変更の後にゴールデンパスを手動で素早く実行する
目的は恐怖を減らすことであり、一晩で完璧なテストスイートを作ることではありません。
UIコンポーネントを整理しても、リデザインを誘発しないには?
まずはレイアウトとスタイルを変えず、予測可能性に注力してください。
安全なUIの整理手順:
- 機能ごとにグループ化する(画面+コンポーネント+ローカルヘルパー)
- 繰り返し現れるマークアップを小さなコンポーネントに抽出する(再設計しない)
- API呼び出しを小さなクライアントモジュールに移し、スクリーンはオーケストレーションだけ行う
- 画面間で読み込み/エラー/空状態を標準化する
抽出ごとに簡単な視覚チェックと一つのエラーケースのトリガーを行ってください。
Koder.ai の機能はリファクタを低リスクにするのにどう役立ちますか?
プラットフォームの安全機能を利用して、変更を小さく可逆に保ちましょう。
実用的なデフォルト:
- リスクの高いファイル移動前にスナップショット/ロールバックを取る
- 深いリファクタ前にソースコードをエクスポートして安定した参照点を持つ
- スコープの境界(まだ変えないもの)をプランニングステップで定義する
- 各ステップで停止できるように小さなインクリメントでデプロイする
これらの習慣は主目的をサポートします: 小さく、取り消せるリファクタで着実に自信を作ること。