レガシーコードのリファクタリング
長期にわたるアウトソーシングプロジェクトはすべて負債を蓄積します。800行に膨れ上がったコントローラー、ビューテンプレートに埋め込まれたビジネスロジック、12ファイルに散らばった重複したバリデーション。クライアントもそれが問題であることを知っています。「シンプルな変更」が1週間かかるたびに実感します。しかし、テストなし、計画なし、6か月前にローテーションしたチームでリファクタリングすることは、関係を損なうリグレッションを引き起こす方法です。Sun Agent Kit は負債を調査し、テストによるセーフティネット付きの段階的なリファクタリング計画を構築し、アプリケーションがすべてのステップで動作し続けるシーケンスで各抽出を実行します。
概要
目的: 長期稼働プロジェクトで蓄積された技術的負債を既存機能を壊すことなく体系的に抽出・再構築する
所要時間: フォーカスしたモジュールで1〜3時間(リスクの高い手動リファクタリングの場合は1〜3日)
使用エージェント: scout、planner、implementer、tester、reviewer
コマンド: /sk:scout、/sk:plan、/sk:cook、/sk:test、/sk:code-review
前提条件
- Sun Agent Kit がインストール・認証済みであること(インストールガイド)
- クリーンな作業ツリーの git リポジトリ(開始前にすべてをコミットまたはスタッシュする)
- 少なくとも部分的なテストスイート(低いカバレッジでもないよりまし)
- 対象とするモジュールまたは機能領域の明確さ(「全部リファクタリング」は試みない)
ステップ別ワークフロー
ステップ 1: 技術的負債を調査して負債マップを構築する
1行も触れる前に、何が問題でなぜかの全体像を把握します。
/sk:scout "large classes, long methods, duplicated logic, missing tests, god objects, business logic in controllers, raw SQL in views"
実行内容: エージェントが以下を実行します:
- 肥大化したファイル・長いメソッド・重複したコードブロックなど、構造的な負債の指標をコードベースでスキャンする
- 複数の無関係な関心事を扱うゴッドオブジェクト(クラス)を特定する
- 依存関係のもつれと循環参照を検出する
plans/reports/に保存される負債マップレポートを生成する
ステップ 2: リファクタリングシーケンスを計画する
エージェントは依存関係を考慮した順序付きの計画を作成します。まだ分離していないクラスに依存するインターフェースは抽出できません。
/sk:plan "Refactor OrderService.js — extract pricing, inventory, and notification concerns into separate services. Preserve all existing behavior."
実行内容: エージェントが以下を実行します:
- 対象ファイルの依存関係グラフ(メソッド・外部依存関係・呼び出し箇所)を分析する
- 独立して抽出できる関心事を特定する
- 各ステップが安全になるよう抽出順序を決定する(リファクタリング途中に循環依存が発生しないよう)
- 推定ステップ数とともに計画を
plans/に保存する
ステップ 3: 本番コードに触れる前に特性テストを追加する
これがセーフティネットです。特性テストは現在の動作をロックします。コードがすべき動作ではなく、実際にしている動作をテストします。リグレッションは見逃しようがなくなります。
/sk:cook "Add characterisation tests for OrderService.js — cover all public methods, capture current return values and side effects including edge cases"
実行内容: エージェントが以下を実行します:
- すべてのパブリックメソッドを分析し、副作用(DB 書き込み、メール送信、ログ)をカタログ化する
- 副作用のスパイセットアップを含む現在の動作をキャプチャするテストスイートを生成する
- 生成されたテストを未変更のコードに対して実行し、ベースラインがパスすることを確認する
ステップ 4: 最初のサービスを抽出する(PricingService)
一度に1つの関心事を抽出します。各抽出は必要であればレビューして元に戻せる十分小さなサイズです。
/sk:cook "Extract PricingService from OrderService.js — move calculateTotal(), applyDiscount(), calculateTax(), formatCurrency() into src/services/PricingService.js"
実行内容: エージェントが以下を実行します:
- 指定されたメソッドを新しいサービスファイルに移動する
- 依存性注入を通じて新しいサービスに委譲するよう元のファイルを更新する
- 動作変更がないことを確認するために特性テストを実行する
- リグレッションがないことを確認するためにフルテストスイートを実行する
ステップ 5: 残りの関心事の抽出を繰り返す
InventoryService と NotificationService にも同じパターンを実行します。各ステップは数分かかり、テストスイートがすべてを検証します。
/sk:cook "Extract InventoryService from OrderService.js — move checkStock(), reserveStock(), releaseStock() into src/services/InventoryService.js"
/sk:cook "Extract NotificationService from OrderService.js — move sendOrderConfirmation(), sendShippingUpdate(), sendCancellationEmail() into src/services/NotificationService.js"
実行内容: 各抽出ごとに、エージェントはメソッドを移動し、委譲を更新し、特性テストとフルテストスイートの両方を実行してリグレッションがゼロであることを確認します。
ステップ 6: リファクタリングしたコードをレビューしてコミットする
すべての抽出が完了したら、コードレビューを実行してリファクタリングされた構造が計画と一致していることを確認します。
/sk:code-review --pending
実行内容: エージェントが以下を実行します:
- 構造的な変更(ファイルサイズの削減、新しいサービスファイル、関心事の分離)をレビューする
- すべての抽出されたサービスで依存性注入が一貫していることを確認する
- 新たなコードの臭いが導入されていないことを確認する
- 達成された負債削減のサマリーを作成する
完全な例: 2年物のモノリスのモジュール抽出
シナリオ
ベトナムのフィンテッククライアントは、2年物の Laravel モノリスを持っています。シンプルなローン管理ツールとして始まったものが、4チームによる連続的な機能追加を経て、大規模なコードベースになりました。LoanController はローン申込、与信スコアリング、返済スケジューリング、SMS 通知、PDF 生成、会計仕訳を処理しています。現在のチーム(6か月前にローテーション)はコードに触れることを恐れています。「シンプルな変更」のたびに予期しない何かが壊れます。クライアントは新しいモバイル API を求めていますが、コントローラーは現在の状態では安全に拡張できません。
連鎖コマンド
# Week 1 Day 1 — understand the full scope of debt
/sk:scout "large classes, duplicated logic, missing tests, god objects, business logic in controllers, circular dependencies"
# Generate a prioritised refactoring plan
/sk:plan "Create phased refactoring plan for LoanController.php — extract CreditScoringService, RepaymentService, NotificationService, PDFService, AccountingService"
# Day 2 — safety net first
/sk:cook "Add characterisation tests for LoanController — cover all public methods, capture all side effects"
# Day 3 — start with the least-entangled service
/sk:cook "Extract CreditScoringService from LoanController — move all credit evaluation logic"
# Day 4 — next extraction
/sk:cook "Extract RepaymentService from LoanController — move schedule generation, payment processing, late fee calculation"
# Week 2 — continue extractions
/sk:cook "Extract NotificationService from LoanController — SMS and email handling, templating"
/sk:cook "Extract PDFService from LoanController — loan agreement and statement generation"
/sk:cook "Extract AccountingService from LoanController — journal entry creation, GL posting"
# Final — review the completed refactoring
/sk:code-review --pending
# Document the new architecture for the team
/sk:docs "Generate architecture documentation for the new service layer — class diagram, dependency graph, how to add a new loan type"
結果
2週間かけて、LoanController は大規模なゴッドオブジェクトから薄いオーケストレーションレイヤーに縮小されます。5つの専用サービスが独立してテスト可能になります。新しいモバイル API エンドポイントは、コードに一度も触れたことがないジュニアエンジニアが抽出されたサービスを直接使用して追加します。クライアントのコメント: 「まるで別のコードベースのようです。」
時間比較
| タスク | 手動 | Sun Agent Kit 使用 |
|---|---|---|
| コードベース全体の技術的負債マッピング | 4時間 | 数分 |
| 抽出シーケンスの安全な計画立案 | 2時間 | 数分 |
| 特性テストの作成 | 3時間 | 数分 |
| 各サービスの抽出と検証 | 60分 × 5 | 数分 × 5 |
| リファクタリング結果のコードレビュー | 60分 | 数分 |
| チームドキュメントの更新 | 90分 | 数分 |
| 合計(5サービス抽出) | 約16時間 | 約2時間 |
ベストプラクティス
1. 抽出前に特性テストを作成する ✅
特性テストスイートは、何も壊れていないことを確認する唯一の信頼できる方法です。テストなしに抽出を始めないでください。数分で生成されるテストスイートは、3週間後に本番環境でリグレッションを発見するより無限に優れています。
2. PR ごとに1つの関心事を抽出する ✅
小さくフォーカスされた PR はレビュー可能です。PricingService と InventoryService を抽出してコントローラーをリファクタリングしてテストを追加する PR はレビュー可能ではありません。それはマージして祈るものです。各抽出は論理的な単位: 1サービス、1 PR、1レビュー、1マージ。エージェントが生成する計画はこの方法でシーケンスされています。
3. 同じブランチでリファクタリングと新機能追加を混在させない ❌
「ここにいる間にクリーンアップしてしまおう」という誘惑があります。これは2つの独立した変更を混同し、リグレッションの帰属を不可能にします。リファクタリング + フィーチャーブランチでバグが見つかった場合、どちらが原因かわかりません。リファクタリングブランチは純粋に保ってください。新しい動作なし、再構築のみ。
4. 「負債をすでに知っている」からといってスカウトステップをスキップしない ❌
コードベースで6か月間作業したすべての開発者は、すべての問題を知っていると思っています。スカウトは親しみが隠す負債を確実に発見します。異なるエンジニアが書いたファイルに散らばった重複ロジック、または「シンプルな」変更が多数のファイルを触ることを要求する理由を説明する循環依存。計画する前に負債マップを読んでください。
トラブルシューティング
問題: 未変更のコードで特性テストが失敗する
解決策: これは現在のテストに環境依存(データベース状態、外部 API コール、時刻依存のロジック)があり、特性ジェネレーターが考慮していなかったことを意味します。/sk:cook "Fix characterisation test environment setup — add database seeding and mock external calls" を実行してください。目標はどのマシンでもパスする決定論的なテストスイートです。
問題: 抽出後、ユニットテストはパスするが結合テストが失敗する
解決策: 抽出により、ユニットレベルでは見えなかったモジュールレベルの変数またはシングルトンを通じて共有状態を持つ2つのサービスが明らかになることがあります。/sk:debug "integration test failures after service extraction" を実行して失敗を説明してください。エージェントが共有状態を見つけ、明示的に注入するかコンフィギュレーションレイヤーに移動するかを提案します。
問題: 抽出されたサービスにコンストラクタパラメーターが多すぎる(コンストラクタの肥大化)
解決策: サービスのコンストラクタパラメーターが7つ以上あることは、サービス自体をさらに分割する必要があるサインです。/sk:plan "split [ServiceName] — identify sub-concerns" を実行して第2レベルの抽出計画を取得してください。エージェントはサービス内のどのメソッドグループが自然にまとまるかを特定します。
問題: リファクタリングマージ後にインポートパスの変更によりクライアントの CI パイプラインが失敗する
解決策: 移動したファイルのリストを添えて /sk:cook "Update all import paths after service extraction — find and replace old module references" を実行してください。エージェントはコードベース全体のすべてのインポート文の更新を生成します。
次のステップ
- コードレビュー — マージ前にすべての抽出 PR をコードレビューでゲートする
- パフォーマンス最適化 — リファクタリング後、再構築されたコードをプロファイリングして意図しないパフォーマンスへの影響がないことを確認する
- ドキュメント自動生成 — 次のチームローテーションが謎ではなくマップから始められるよう、新しいアーキテクチャをドキュメント化する
重要なポイント: レガシーコードは一度にすべてを書き直すことでリファクタリングされるのではなく、途中で何も壊れないことを保証するテストセーフティネットとともに、一度に1つの抽出されたサービスずつ段階的にリファクタリングされます。