AI駆動型のリファクタリングによる関数の抽出とカプセル化の自動化

AIリファクタリング実践論:スパゲッティコードを安全に解体する関数抽出とカプセル化の自動化

この記事は急速に進化する技術について解説しています。最新情報は公式ドキュメントをご確認ください。

約14分で読めます
文字サイズ:
AIリファクタリング実践論:スパゲッティコードを安全に解体する関数抽出とカプセル化の自動化
目次

この記事の要点

  • AIによる関数抽出とカプセル化の自動化
  • スパゲッティコードやレガシーコードの安全な解体
  • GitHub Copilot/CursorなどAIコーディング支援ツールの活用

リファクタリングの「恐怖」とAIによるパラダイムシフト

深夜2時、緊急のバグ修正。目の前にあるのは、ドキュメントが存在せず、幾人のエンジニアの手によって継ぎ接ぎされた数千行の「神クラス(God Class)」だ。変数のスコープは曖昧で、どこで何が変更されているか追うだけで脳のメモリが溢れそうになる。このコードに手を入れることは、地雷原を歩くようなものだ。皆さんも、こんな冷や汗をかくような経験はないだろうか?

エンジニアがリファクタリングを躊躇する最大の理由は「恐怖」にある。既存の動作を壊してしまう恐怖、隠れた依存関係を見落とす恐怖だ。しかし、AI駆動開発の時代において、私たちはこの恐怖に対する強力な対抗手段を手に入れた。それが、AIによる「コンテキスト認識」と「自動化された安全網」である。

なぜレガシーコードの関数抽出は失敗しやすいのか

従来のリファクタリングツール(IDEの組み込み機能など)は、構文的な構造操作には長けているが、「意味」を理解していない。例えば、あるロジックを別関数に切り出す際、IDEは変数の参照関係は解決してくれるが、そのロジックが持つ「ビジネス上の意図」や、グローバルな状態に依存する「副作用」までは考慮してくれない。結果として、形だけ整ったが挙動がおかしくなる、あるいは可読性がかえって下がるという事態を招く。

人間が手動で行う場合も同様だ。認知負荷が高すぎるため、複雑に絡み合った条件分岐を正確に解きほぐすのは至難の業である。ここで多くのプロジェクトが「触らぬ神に祟りなし」と判断し、技術的負債が雪だるま式に増えていく。経営視点で見れば、これは将来の開発生産性を著しく削ぐ重大なリスクだ。

AIが担う3つの役割:理解・提案・検証

大規模言語モデル(LLM)を搭載したAIコーディングアシスタント(GitHub CopilotやCursorなど)は、この状況を打破する。

  1. 理解(Understand): コード全体を読み込み、変数の意味や処理の流れを自然言語で説明できる。
  2. 提案(Suggest): 副作用を最小限に抑えた関数抽出や、適切な命名を提案できる。
  3. 検証(Verify): リファクタリング前後の挙動を保証するテストコードを生成できる。

AIは単にコードを書くスピードを上げるツールではない。複雑性をハンドリングするための「認知の拡張」ツールなのだ。AIをペアプログラマーとして適切にハンドリングすれば、リファクタリングに伴うリスクを大幅に低減し、工数を従来の半分以下に圧縮することも十分に可能である。

本チュートリアルのゴール:神クラスの解体

本記事では、Eコマースシステムにおける肥大化した決済処理コード(スパゲッティコード)を題材としよう。TypeScriptを用いて、以下のステップで安全にリファクタリングを進めていく。

  1. 現状のロジックを解析し、仕様を把握する。
  2. 意味のある単位でロジックを純粋関数として抽出する。
  3. 関連するデータとロジックをクラスにまとめ、カプセル化する。
  4. テストを自動生成し、挙動が変わっていないことを保証する。

AIに丸投げするのではなく、エンジニアが司令塔となり、AIの手足を使って外科手術のようにコードを改善していくプロセスを、ぜひ一緒に体感していただきたい。

2. 環境構築と「診察」の準備

いきなりコードを書き換えてはいけない。医者が手術の前に詳細な検査をするように、まずは対象コードの現状を正確に把握する必要がある。「まず動くものを作る」プロトタイプ思考においても、現状のベースラインを知ることは不可欠だ。

必要なツールセット(IDE + 生成AIプラグイン)

今回の実践では、以下の環境を推奨する。

  • IDE: VS Code または Cursor
  • AI Assistant: GitHub Copilot または Cursor
  • 言語: TypeScript(静的型付け言語はAIへのコンテキスト提供において有利であり、リファクタリング精度が向上する)

特筆すべきは、近年のAIコーディングツールの進化だ。CursorやGitHub Copilotの最新版では、単一ファイルだけでなくプロジェクト全体のコンテキスト(@Codebase@workspace といったコマンドで指定)をAIに理解させる機能が強化されている。これにより、ファイルまたぎの複雑な依存関係も把握しやすくなった。また、利用可能なAIモデルも多様化しており、タスクの性質に合わせて最適なモデルを選択できる環境も整いつつある。

対象コードの現状把握:AIによる依存関係マップの作成

まず、リファクタリング対象のコードをAIに「診察」させる。いきなり修正を依頼するのではなく、現状の仕様を言語化させることが重要だ。以下のようなプロンプトを投げてみよう。

Prompt:
@Codebase または @workspace を使用して)
現在開いている PaymentProcessor.tsprocessOrder メソッドは非常に複雑で長大です。このメソッドが何を行っているか、以下の観点で要約してください:

  1. 主要な処理フロー
  2. 外部システムへの依存(DB接続、APIコールなど)
  3. 変更されている状態(副作用)
  4. 潜在的なリスクやバグの可能性

AIの回答によって、コードの中に潜む「時限爆弾(副作用)」の位置を特定できる。例えば、「在庫を減らす処理の直後に、なぜかユーザーのランク更新処理がハードコードされている」といった、一見関係なさそうなロジックの密結合を発見できるはずだ。最新のAIモデルであれば、コードの意図を汲み取った精度の高い分析が期待できる。

リファクタリング前の安全網:AIによる現状仕様のテスト化

レガシーコードの最大の問題は「テストがない」ことだ。テストなしにリファクタリングを行うのは、命綱なしで崖を登るようなものであり、自殺行為に等しい。しかし、テストを書くには仕様を完全に理解しなければならないというジレンマがある。

ここで「スナップショットテスト」的なアプローチをAIに行わせることで、現状の挙動を固定する。

Prompt:
processOrder メソッドの現在の挙動を保証するための単体テストケースを作成してください。

  • Jestを使用(プロジェクトのテスト環境に合わせて変更可)
  • 正常系だけでなく、在庫不足や決済エラーなどの異常系も含める
  • 実装の内部構造には依存せず、入力と出力、およびモックされた外部呼び出しの検証に焦点を当てる(ブラックボックステスト)
  • 既存のバグと思われる挙動も含めて、今の動きを再現するテストにすること

AIが生成したテストを実行し、現在のコードで「Pass」することを確認する。これが、これからの作業の命綱(セーフティネット)となる。もしAIが生成したテストが失敗するなら、それはAIのコンテキスト理解不足か、コード自体に実行時エラーが含まれている可能性がある。まずはテストが通る状態を作り、足場を固めてからスタートラインに立つことが鉄則だ。

3. Step 1: 意味のある単位でのロジック切り出し(関数抽出)

環境構築と「診察」の準備 - Section Image

準備が整ったら、最初の一手だ。巨大な関数を、意味のある小さな単位に分割していく。

AIへの指示出し:選択範囲の意図を汲み取る

例えば、以下のようなコードブロックがあったとする。

// Before: 混在したロジック
public async processOrder(order: any) {
  // ...
  let total = 0;
  for (const item of order.items) {
    let itemPrice = item.price;
    if (item.category === 'ELECTRONICS' && order.user.isPremium) {
      itemPrice *= 0.9; // 10% off
    }
    if (item.quantity > 5) {
      itemPrice *= 0.95; // Bulk discount
    }
    total += itemPrice * item.quantity;
  }
  
  if (total > 10000) {
    total -= 500;
  }
  // ...
}

価格計算のロジックがメイン処理に埋め込まれている。これを選択し、AIにリファクタリングを指示するのだが、ここで重要なのは「ツールを正しく使う」ことだ。

現在の主要なAIコーディングツールでは、コードをコピーしてチャット欄に貼り付ける古いやり方は非効率と言える。コードを選択した状態でインラインチャット(Cmd+I / Ctrl+I)やエージェントモードを起動し、その場で修正を適用するのが定石だ。特に最新機能では、エージェントが自律的にコードの文脈を理解し、より安全な変更を提案できるよう進化している。

コンテキストを維持したプロンプト設計

単に「関数に抽出して」と言うだけでは不十分だ。AIにプロジェクト全体の構造を理解させる必要がある。ここで決定的な役割を果たすのが、@workspace コマンドだ。

このコマンドを使用することで、AIは現在開いているファイルだけでなく、プロジェクト全体の型定義、インターフェース、共通ユーティリティなどを参照範囲(コンテキスト)に含めることができる。これにより、AIは単なるテキスト操作ではなく、既存のコードベースと整合性の取れたリファクタリングを提案可能になる。

クラスのメソッドにするのか、プライベート関数にするのか、変数はどう渡すのか。具体的な指示が品質を左右する。

Prompt:
@workspace /extract
選択した範囲の価格計算ロジックを、calculateTotalPrice という名前のプライベートメソッドとして抽出してください。

制約条件:

  1. 引数は必要なデータ(itemsとユーザーの属性)のみを受け取るようにし、order オブジェクト全体への依存を避けてください。
  2. 副作用を持たない純粋関数(Pure Function)として実装してください。
  3. 計算ロジック(割引ルールなど)が明確になるよう、必要であれば内部でさらに小さなヘルパー関数を作成しても構いません。
  4. 使用するモデルは推論能力の高いモデルを使用してください。

副作用の検出と純粋関数化の試み

AIはこの指示を受け、以下のようなコードを提案してくるだろう。最新のモデルは、変数のスコープや依存関係の分析精度が飛躍的に向上しており、複雑な依存関係もきれいに解きほぐしてくれる。

// After: 抽出されたメソッド
private calculateTotalPrice(items: Item[], isPremiumUser: boolean): number {
  let total = 0;
  for (const item of items) {
    total += this.calculateItemPrice(item, isPremiumUser) * item.quantity;
  }
  
  if (total > 10000) {
    total -= 500;
  }
  return total;
}

private calculateItemPrice(item: Item, isPremiumUser: boolean): number {
  let price = item.price;
  if (item.category === 'ELECTRONICS' && isPremiumUser) {
    price *= 0.9;
  }
  if (item.quantity > 5) {
    price *= 0.95;
  }
  return price;
}

注目すべきは、order 全体ではなく itemsisPremiumUser だけを受け取るように変更された点だ。これにより、このメソッドは order の構造変更の影響を受けにくくなり、単体テストも容易になる。

AIは「依存を最小化する」というエンジニアリングの原則を、的確なプロンプトとコンテキスト提供によって忠実に実行してくれる。もしAIの提案が意図と異なる場合は、使用するモデルを切り替えてみるのも有効な手段だ。論理的な複雑さが高いリファクタリングには、思考プロセスを持つ推論特化型モデルが適している場合が多い。

4. Step 2: データとロジックの再結合(カプセル化)

4. Step 2: データとロジックの再結合(カプセル化) - Section Image 3

関数抽出を進めていくと、クラス内にプライベートメソッドが乱立し、引数リストが長くなる現象(Data Clumps:データの群れ)に遭遇することがある。これは、データとロジックの置き場所が不適切であるサインだ。

散らばった変数をクラス/構造体にまとめる

先ほどの calculateTotalPrice メソッドのように、商品リストとユーザー情報を常にセットで渡しているなら、それは別の概念としてカプセル化すべきかもしれない。

ここでAIにアーキテクチャレベルの提案を求める。

Prompt:
PaymentProcessor クラスから価格計算に関するロジック(calculateTotalPrice, calculateItemPriceなど)を分離し、独立した PriceCalculator クラス、またはドメインモデルとしての Order クラスに移動することを検討しています。凝集度を高め、結合度を下げる観点から、最適な設計変更を提案し、コードを示してください。

AIに提案させる最適なアクセス修飾子

AIは、ドメイン駆動設計(DDD)の知識ベースに基づき、以下のようなリファクタリングを提案する可能性がある。

// 提案: ドメインモデルへのロジック移動
class Order {
  constructor(
    public readonly items: Item[],
    public readonly user: User
  ) {}

  public get totalAmount(): number {
    let total = this.items.reduce((sum, item) => 
      sum + this.calculateItemPrice(item) * item.quantity, 0);
    
    return this.applyBulkDiscount(total);
  }

  private calculateItemPrice(item: Item): number {
    // ...
  }
}

「ロジックをデータ(items, user)の近くに置く」というオブジェクト指向の基本原則に従い、PaymentProcessor は単なる手続き屋から解放され、よりスリムになる。

凝集度を高めるためのインターフェース設計支援

新しいクラスを作成した際、その公開インターフェース(API)をどうするかは重要だ。AIに対して、「このクラスを使うクライアントコード(呼び出し側)の例も作成して」と依頼することで、使い勝手の悪いAPIになっていないかを即座に検証できる。

「呼び出しにくい」と感じたら、それはカプセル化が不十分か、責務が多すぎる証拠だ。AIとの対話を通じて、このフィードバックループを高速に回すことができるのが最大のメリットである。仮説を即座に形にして検証する、まさにプロトタイプ思考の真骨頂と言えるだろう。

5. Step 3: AI駆動の品質検証と回帰テスト

Step 2: データとロジックの再結合(カプセル化) - Section Image

コードがきれいになっても、動かなくなっては意味がない。リファクタリングの最終工程は、執拗なまでの検証だ。

リファクタリング後の単体テスト自動生成

新しく作成した Order クラスや PriceCalculator クラスに対して、改めて単体テストを生成させる。

Prompt:
新しく作成した Order クラスの totalAmount getter に対する単体テストをJestで作成してください。以下のエッジケースを網羅すること:

  • 商品リストが空の場合
  • 割引適用条件の境界値(数量がちょうど5個の場合と6個の場合)
  • 合計金額がマイナスになるような不正なデータ(もしあり得るなら)

AIは境界値分析(Boundary Value Analysis)に基づいたテストケースを高速に列挙してくれる。人間が見落としがちな「0」や「null」、「最大値」といったケースも、プロンプトで指示すれば漏れなくカバーできる。

AIによるコードレビュー:人間が見落とすエッジケース

テストコードだけでなく、AIにレビュアーになってもらうのも有効だ。

Prompt:
以下のリファクタリング後のコードをレビューしてください。特に、パフォーマンスのボトルネックになり得る箇所、型安全性が不十分な箇所、将来的な拡張を阻害する可能性のある箇所を指摘してください。

AIは感情を持たないため、忖度なしに指摘をしてくれる。「このループの中で毎回オブジェクトを生成しているのは無駄です」といった指摘は、冷静に受け止めればコード品質を一段引き上げるチャンスとなる。

新旧コードの挙動比較

最後に、最初に作成した「リファクタリング前のスナップショットテスト」を実行する。構造は激変したが、外部から見た振る舞い(入出力)は変わっていないはずだ。もしテストが落ちたなら、どこかで仕様を変えてしまっている。

AIを活用すれば、テストの修正も「エラーログを貼り付けて修正案を出させる」ことで迅速に行える。バグ修正のサイクルを短縮できるため、リファクタリングの心理的ハードルはさらに下がる。

6. 評価と次のステップ:AIリファクタリングの限界と可能性

ここまで見てきたように、AIはリファクタリングの強力なアクセラレータとなる。しかし、それは「自動運転」というよりは「高度な運転支援システム」に近い。

AIが得意なこと・苦手なこと

  • 得意: 局所的なロジックの抽出、テストケース生成、ボイラープレートコードの記述、構文的な変換、既知のデザインパターンの適用。
  • 苦手: システム全体にまたがるアーキテクチャの刷新、ビジネス要件の深い理解に基づいた設計判断、セキュリティの微妙なニュアンス(コンテキスト依存の脆弱性など)。

例えば、「マイクロサービス化するためにこのモジュールを分割する」といった大規模な判断は、依然としてシニアエンジニアの経験と洞察が必要だ。AIはその判断材料を提供し、決定後の作業をサポートする役割に留まる。技術の本質を見抜き、ビジネスへの最短距離を描くのは、あくまで私たち人間の役割である。

チーム開発におけるAI生成コードのレビュー基準

チームでAIリファクタリングを導入する場合、「AIが書いたから正しい」という先入観は捨てるべきだ。プルリクエスト(PR)には、AIへのプロンプトや、なぜそのようにリファクタリングしたかという意図を明記させるルールを設けることをおすすめする。

また、AI生成コードは時として冗長になる傾向がある。人間の目で見て「過剰な抽象化ではないか?」「YAGNI(You Aren't Gonna Need It)原則に反していないか?」をチェックするフィルターは常に必要だ。

継続的な技術的負債解消に向けて

スパゲッティコードを一度にすべて解消しようとしてはいけない。AIを使えば作業は速くなるが、それでもビッグバン・リファクタリングはリスクが高い。

「ボーイスカウトのルール(来た時よりも美しくして去る)」を適用し、機能追加やバグ修正のついでに、AIを使って周辺コードを少しずつリファクタリングする習慣をつけよう。AIというパートナーがいれば、その「少しの手間」は驚くほど軽くなる。

技術的負債は、放置すれば利子が膨らむ借金だ。しかし、AI駆動のリファクタリングスキルを身につければ、私たちはその借金を着実に、そして効率的に返済していくことができる。さあ、エディタを開き、まずはあの巨大な関数に「解説して」と話しかけるところから始めよう。

AIリファクタリング実践論:スパゲッティコードを安全に解体する関数抽出とカプセル化の自動化 - Conclusion Image

コメント

コメントは1週間で消えます
コメントを読み込み中...