🌟

Cursor Agent を利用したリファクタリングの試み(失敗編)

2025/03/20に公開

概要

Cursor Pro (Claude 3.7-sonnet) を利用して、Flutterアプリのプロジェクト構成を変更する試みを行いました。

対象としたのは、ある程度の規模があり、ユニットテストが整備されている Flutter のリファレンスアプリ(compass_app)です。
構成変更後の妥当性を検証しやすいプロジェクトを選定しました。

ゼロベースでの開発には比較的適していることが分かっていましたが、リファクタリング用途としてどの程度活用できるのか、またどのような点に注意すべきかを検証しました。

失敗編と題しているように、今回の構成変更の結果は失敗に終わり、ユニットテストに通らず、動作もしない状態となりましたが、今後の指示方法やルールの検討に有益な検証となりました。

依頼内容

Flutter公式のリファレンスアプリ compass_app のプロジェクト構成を、レイヤーファーストからフィーチャーファーストへ変更するよう依頼しました。

構成変更が成功すれば、状態管理も provider から riverpod へ移行する予定です。

実行環境

項目 内容
PC MacBook Pro m4
IDE Cursor Proプラン
モデル Claude 3.7 sonnet
消費リクエスト数 約60リクエスト
合計作業時間 約3時間

今回お借りした Cursor のルール:Cursor-Project-Rules

samples リポジトリには他のサンプルも多数あるため、 sparse-checkout にて、 compass_app の app (flutter側) のみをチェックアウトした状態で実施します。

初回の依頼内容

生成AI を用いて 既存のflutterアプリのリファクタリングを行います。  
変更内容は下記の通りです。  
  
- ディレクトリ構造の変更  
layer first の構造から feature first の構造に変更します。  
  
作業を進めるための計画を立ててください。
実行計画は markdown 形式で記載し、適切な名前で docs フォルダに 格納してください。

作業課程

作成された実行計画

ディレクトリ構造リファクタリング計画

背景と目的

現在のアプリケーションコードは、従来のレイヤーベースのディレクトリ構造で構成されています。
この構造では、コード変更時の影響が広範囲に及び、特定の機能に関連するコードを見つけるのが難しくなっています。

本リファクタリングでは、コードを機能(feature)ベースのディレクトリ構造に再編成し、以下の目的を達成します:

  1. 機能単位でのモジュール化により保守性を向上
  2. 関連するコードを近くに配置することによる開発効率の向上
  3. 機能間の依存関係を明確化し、循環参照を排除

現状の構造

lib/
├── config/             # アプリケーション設定
│   ├── di/             # 依存性注入
│   └── routes/         # ルーティング
├── data/               # データ層
│   ├── repositories/   # リポジトリ実装
│   └── services/       # サービス実装
├── domain/             # ドメイン層
│   ├── models/         # モデル定義
│   └── use_cases/      # ユースケース
└── ui/                 # UI層
    ├── activities/     # アクティビティ画面
    ├── auth/           # 認証画面
    ├── booking/        # 予約画面
    ├── core/           # 共通UI要素
    ├── home/           # ホーム画面
    ├── results/        # 検索結果画面
    └── search_form/    # 検索フォーム画面

目標の構造

lib/
├── features/           # 機能ベースのモジュール
│   ├── activities/     # アクティビティ関連
│   │   ├── data/       # データアクセス
│   │   ├── domain/     # ドメインモデル
│   │   └── presentation/ # UI
│   ├── auth/           # 認証関連
│   ├── booking/        # 予約関連
│   ├── home/           # ホーム画面
│   ├── results/        # 検索結果
│   └── search_form/    # 検索フォーム
├── core/               # コアユーティリティ
│   ├── config/         # アプリ全体の設定
│   ├── di/             # 依存性注入
│   ├── navigation/     # ナビゲーション
│   ├── theme/          # テーマ定義
│   └── utils/          # ユーティリティ
└── shared/             # 複数の機能で共有されるコンポーネント

移行戦略

フェーズ1: 準備

  1. 既存コードの分析

    • 各機能に関連するコードの特定
    • 依存関係の洗い出し
  2. Gitスパースチェックアウトの設定

    • 既存のコードをスパースチェックアウトで選択的に取得
    • 機能ごとに段階的に移行する準備

フェーズ2: 移行作業

  1. 機能ごとの移行

    • 優先度の高い機能から順に移行
    • 各機能内で、ドメイン、データ、プレゼンテーション層に分割
    • それぞれのファイルをreact/copy、移動、importパス修正の順に対応
  2. 共通コンポーネントの整理

    • 複数機能で使用されるコンポーネントを特定
    • coreまたはsharedディレクトリに移動

フェーズ3: 検証とリファクタリング

  1. コンパイルエラーの解消

    • 参照パスの修正
    • 依存関係の再構築
  2. 重複コードの排除

    • 類似機能の統合
    • 共通処理のユーティリティ化
  3. テスト実施

    • 機能テスト
    • UI/UXテスト

リスクと対策

リスク 対策
コンパイルエラーの大量発生 機能ごとに段階的に移行し、各段階でコンパイルエラーを修正
循環参照の発生 依存関係を明確にし、必要に応じてインターフェースによる疎結合化
ビルド時間の増加 移行中はスパースチェックアウトを活用して必要なコードのみをビルド
既存機能の破損 十分なテストカバレッジの確保と段階的なデプロイ

タイムライン

  1. 準備フェーズ: 1週間
  2. 各機能の移行: 3週間(機能ごとに3-5日)
  3. 検証とリファクタリング: 1週間
  4. ドキュメント更新とレビュー: 3日

成功基準

  1. すべての機能が新しいディレクトリ構造で正常に動作すること
  2. コンパイル時間が増加していないこと
  3. コードの保守性が向上していること(静的解析ツールで計測)
  4. 開発チームがディレクトリ構造を容易に理解できること
コミットの履歴
64fd4c59 refactor: パターンマッチングの構文をis/asに変更して型安全性の警告を解消
540916d2 style: ディレクティブの順序と不要なキャストを修正
36ec58f6 refactor: すべてのViewModelでResultパターンマッチングと型を修正
8a0d7a27 fix: ResultsViewModel.configをitineraryConfigに修正
4463a88a refactor: ビューモデルのResultパターンマッチング修正
68bb9444 fix: 残りのインポートパスとCommand型の問題を修正
4fe94049 fix: モデルのインポートパスとAPIサービスの参照先を修正
172d2d94 refactor: Bookingフィーチャーのインポートパスとパターンマッチングを修正
c81c5440 refactor: リポジトリとサービスレイヤーのインポートパスとパターンマッチングを修正
a32d0ff9 refactor: ViewModelとUseCaseのインポートパスとResult型パターンマッチングを修正
c4af5280 refactor: 検索フォーム関連のインポートパスとResult型のパターンマッチングを修正
6ec46c1f refactor: ルーターと検索フォーム画面のインポートパスを修正
7f980f20 refactor: Homeフィーチャーのインポートパスと型マッチングを修正
65e0dfa4 refactor: Results表示レイヤーのインポートパスを修正
165a12b4 refactor: Coreコンポーネントと表示レイヤーのインポートパスを修正
605c88b0 refactor: Result型の処理方法を修正(castを明示)
7c49ecd3 refactor: Auth ViewModelのインポートパスとResult型の処理を修正
7f65dd57 refactor: ActivitiesViewModelのインポートパスとResult型の処理を修正
5272a809 refactor: Bookingユースケースのインポートパスを修正
61efce2b refactor: バックエンドのインポートパスを修正
ebec6574 refactor: auth presentationレイヤーのインポートパスを修正
ed9f25fc refactor: core/uiコンポーネントのインポートパスを修正
751676c7 refactor: activitiesフィーチャーのインポートパスを修正
135f3a03 refactor: インポートパスを更新(各フィーチャー)
ccf01d2a refactor: インポートパスの更新と共有データの移行
5dde9fd8 refactor: ディレクトリ構造変更(フェーズ1)完了
ce06d6e4 refactor: Search Formフィーチャーのファイル移行
f02ba8b3 refactor: Resultsフィーチャーのファイル移行
b115cda5 refactor: Homeフィーチャーのファイル移行
3a657591 refactor: Bookingフィーチャーのファイル移行
e617308c refactor: Authフィーチャーのファイル移行
71ccf6dd refactor: Activitiesフィーチャーのファイル移行
609690d1 refactor: 基本ディレクトリ構造の作成とコアレイヤーのファイル移行
c16c0ddf docs: ディレクトリ構造変更計画の追加

作業中の出来事

人間がディレクトリ構成を変更する場合、通常は IDE の機能を使い、import の修正も同時に行います。しかし、生成AIは mkdir や mv などのコマンドでファイル操作を行うため、import の変更が追従しません。その結果、ディレクトリ構成の移動と import の修正が別タスクとなる という問題が発生しました。

特に、import に移行前のパスが残っていた影響で、レイヤーファーストのディレクトリやファイルが削除されずに残ったままになっていました。その状態で feature ディレクトリを新規作成し、ファイルを移動させたため、同じクラスを持つファイルが2つ存在する状況 になっっていました。

その結果、クラスの参照エラーが発生し、AIが不要なロジック変更を加え始めました。変更により静的解析のエラーは解消されましたたが、今度はユニットテストが失敗する という別の問題が発生しました。

ここで、今回の目的を再度伝え、「ロジックの変更は不要 であり、移行前のディレクトリを削除すべき」と指示しました。しかし、AIは削除によるリスクを懸念し、シンボリックリンクを作成しようとしたため、この辺りで、作業を中断させました。

作業の振り返り

ロジックを変更し始めたあたりで雲行きが怪しくなっていましたが、検証目的でしたので、続行させてみました。
結果として、動作せず、ユニットテストにも失敗している状況となってしまいました。
今後に向けて、大きく下記の点に注意すると良いと感じました。

DON'T を明確に伝える

今回は、「import の変更のみで、ロジックの変更は禁止とする」ことを明確に指示しておけば、状況が違った可能性があります。
最後に ユニットテスト を実施させ、成功することをゴールとする予定でしたが、
ロジック変更に伴い、ユニットテストに手を加えては、今回の目的から外れてしまうため、

事前に DON'T を伝えておくと、良い結果が得られた可能性もあります。

今回は 検証目的 でしたので、殆ど全ての変更を許可しましたが、ロジック変更をした時点で変更を拒否することも出来るため、実際には拒否した上で、軌道修正をすることになるでしょう。

影響範囲の事前チェック

普段なら事前に行う作業ですが、import修正の際に、何度も変更対象を確認したり、コミット後も変更漏れが残っている、ということがありました。
事前に影響範囲をリストアップさせ、作業時に参照させれば、効果的だと感じました。これは、ルールに書くと良いでしょう。

タスクの分割方法を変える

個々のタスク(コミット)は機能別に実施されていたものの、全体工程は、

  1. ディレクトリの移動(コピー)
  2. import の修正

となっており、1 の時点ではエラーを許容せざるを得ませんでした。
結果 2 のimportエラー修正の工程で、全てのファイルを修正するまで、成否がわからない事態となりました。

改善策として、今回のケースでいえば、まずは HOME に関する機能について対応させ、
ディレクトリの移動 と import の変更 をセットで作業した方が、細かく検証ができたでしょう。
試しに、1機能対応させ、最適な方法を計画するというステップがあると良いでしょう。

その他

import を変更した際に、並び順が不適切となっており、lint情報が出ていました。
修正指示を出したところ、ファイル1つ1つの並び順を手動(自動ですが。。)で、修正していました。
この辺りは、dart fixdart format で修正させた方が、効率が良さそうです。
手順に含めると良いでしょう。

AIの振り返り

AIにさせた「振り返りと改善策」の提案

ディレクトリ構造リファクタリングの教訓と改善策

発生した問題

今回のディレクトリ構造リファクタリングでは、以下の問題が発生しました:

  1. フォルダ構成とimportパスの変更だけでなく、ロジック自体に変更が加わってしまった
  2. その結果、ユニットテストが失敗する事態が発生
  3. 修正範囲が想定より広がり、予期せぬ副作用が生じた

改善策と次回のアプローチ

1. 明確な境界設定

  • 変更範囲の厳密な定義:リファクタリングの範囲を「ディレクトリ構造とimportパスの変更のみ」と明確に定義する
  • 自動チェックの導入:ロジック変更を検出する自動チェックを導入(diffツールの活用)

2. 段階的なアプローチ

1. ディレクトリ構造の作成 → 2. ファイルの移動 → 3. importパスの修正 → 4. テスト

各ステップ後に検証を行い、問題が発生した場合は早期に対処します。

3. 具体的な手順

準備フェーズ

  1. 作業ブランチの作成

    git checkout -b refactor/directory-structure
    
  2. 移行計画の詳細化

    • 各ファイルの移動先を明確にしたマッピング表の作成
    • 依存関係グラフの作成(依存の少ないファイルから移行するため)
  3. 自動化スクリプトの準備

    • ファイル移動とimportパス修正のスクリプト作成
    • 変更内容の検証スクリプト作成

実行フェーズ

  1. ディレクトリ構造の作成

    mkdir -p lib/features/{activities,auth,booking,home,results,search_form}/{data,domain,presentation}
    mkdir -p lib/core/{config,di,navigation,theme,utils}
    mkdir -p lib/shared
    
  2. ファイルの移動(コピー→テスト→削除の順で)

    • 各ファイルをコピーして新しい場所に配置
    • インポートパスはまだ変更せず、コンパイルエラーを回避
    • ビルド確認後、元のファイルを削除
    # 例: activityモデルの移動
    cp lib/domain/models/activity/activity.dart lib/features/activities/domain/models/
    # ビルド確認後
    rm lib/domain/models/activity/activity.dart
    
  3. import パスの修正

    • 1ファイルずつimportパスを修正
    • 各修正後にビルドとテストを実行
    # 例: booking_viewmodel.dartのimportパス修正
    sed -i 's#import "../../../../domain/models/activity/activity.dart"#import "../../domain/models/activity.dart"#g' lib/features/booking/presentation/view_models/booking_viewmodel.dart
    
  4. 段階的なテスト実行

    • 各機能モジュールの移行完了後にユニットテスト実行
    • 失敗したテストは即座に修正(ロジック変更せず)

検証フェーズ

  1. コードレビュー

    • ロジック変更がないことを確認するためのレビュー
    • diff確認に特化したレビュープロセスの導入
  2. 自動テスト実行

    flutter test
    
  3. マニュアルテスト

    • 主要なユースケースの実行確認
    • UI/UXに変更がないことの確認

4. 重要なポイント

  • import参照のシンボリックリンク活用

    • 移行中は古いパス構造へのシンボリックリンクを作成し、段階的に移行
  • リファクタリングツールの活用

    • IDEの自動リファクタリング機能を活用
    • Flutter専用のリファクタリングツールを検討
  • 変更の最小化

    • 1つのPRでは1つの関心事(例:1つの機能モジュールの移行)のみに集中
    • コードフォーマットの変更は別PRで行う
  • テスト駆動のリファクタリング

    • 既存のテストが通ることを常に確認
    • 「テスト→リファクタリング→テスト」のサイクルを厳守

チェックリスト

リファクタリング作業中に以下のチェックリストを使用します:

  • 変更はディレクトリ構造とimportパスのみに限定されているか
  • ファイル内の実際のコード(ロジック)は変更されていないか
  • 各変更後にビルドとテストが正常に完了するか
  • 不必要なコードフォーマット変更は含まれていないか
  • すべてのテストが正常に実行されるか
  • ドキュメントは更新されているか

まとめ

結果として、ユニットテスト、アプリ動作 ともに失敗する状態で終了となりましたが、
今後に活かせる結果が得られたので良かったと思います。
「振り返りと改善策」をもとに指示内容を調整して、また試してみようと思います。

既存のユニットテストに成功した状態で終える、という趣旨からは外れますが、
リバースエンジニアリングさせて1から作り直す。とした方が、エラーは少なかったでしょう。
枠に余裕があれば、こちらも試してみたいところです。

provider から riverpod への変更も難易度上がりそうですが、
検証内容をもとに、指示やルールを調整してチャレンジしてみようと思います。

GitHubで編集を提案

Discussion