はじめに
前回までは、CursorおよびRoo-Code(Roo-Clien)を使用し、簡単なゲーム作成などを行ってきました。今回は、より実践的にコードのリファクタリングにトライしてみようと思います。
これまでの記事、
・初めての「Cursor」
・初めての「Cursorでゲーム作成」
・初めての「Cursorでゲーム作成(2)」
・初めての「Roo-Cline」
リファクタリング対象
普段の開発業務で既存のソースを修正する際、下記のようなパターンに遭遇された方は多いと思います。
・関数の行数が多く、スパゲッティコードのため、ロジックがわかりづらい(可読性が低い)
・コメントと実際のロジックが一致しない
・処理するデータ量は大きくないが、なぜか処理速度が遅い
上記のパターンを満たす改善しがいのあるコードとして、以下のようなJavaのサンプルコードを準備しました。
package com.test.cusor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
public class Main {
public static void main(String[] args) {
//対象者リストを取得する。
List<Map<String,Object>> list = getPersons();
String[] names = {"山田太郎", "渡辺美咲", "加藤翔", "小林優子", "田中愛子"};
// namesに保存された名前を、listのnameに保存された名前にマッチするPersonを取得して、list2に保存する。
List<Map<String,Object>> list2 = new ArrayList<>();
for (int i = 0; i < names.length; i++) {
for (int j = 0; j < list.size(); j++) {
if (names[i].equals(list.get(j).get("name"))) {
list2.add(list.get(j));
}
}
}
//list2を年齢の昇順でバブルソートする。
for (int i = 0; i < list2.size() - 1; i++) {
for (int j = 0; j < list2.size() - 1 - i; j++) {
int age1 = (Integer) list2.get(j).get("age");
int age2 = (Integer) list2.get(j + 1).get("age");
if (age1 < age2) {
Map<String,Object> temp = list2.get(j);
list2.set(j, list2.get(j + 1));
list2.set(j + 1, temp);
}
}
}
//list2を出力する。
System.out.println(list2);
}
private static List<Map<String,Object>> getPersons() {
List<Map<String,Object>> list = new ArrayList<>();
String[] names = {"山田太郎", "佐藤花子", "鈴木一郎", "高橋健太", "田中愛子",
"伊藤誠", "渡辺美咲", "中村拓也", "小林優子", "加藤翔"};
Random random = new Random();
for (int i = 0; i < 10; i++) {
Map<String,Object> map = new HashMap<>();
map.put("id", i + 1);
map.put("name", names[i]);
map.put("age", random.nextInt(60) + 20); // 20-79歳の範囲でランダム
list.add(map);
}
return list;
}
}
リファクタリング実践
まず、Cursorでリファクタリングできるかを試してみます。新しいチャットを開いて、「ソースをリファクタリングしてください。」を入力して実行すると下記のようにリファクタリング前後のハイライトで表示して、問題がなければエディタ下のAcceptFileボタンをクリックすると、Main.javaに反映します。

リファクタリング後のソースを見ると、下記のような修正がされていました。
・Personクラス作成をし、リストの項目をMapからクラスに変更
・main関数を簡素化するため、キーリストでデータ抽出するロジックとソートロジックを別関数に移行
・バブルソートをJava言語で推奨したソート方式に変更
「リファクタリングをしてください」という曖昧な要望にも、ある程度応えてくれることがわかります。一方で、以下のような未解消の改善点や、新しい問題も出てきました。
・コメントとロジック不一致
・既存のコメントがすべて消去された
・内部クラスや同じファイルに複数クラスを定義された
ファイルの分離
上記の問題を解決するために、次は「Personを別ファイルに保存してください。」と入力して実行してみました。リファクタリング結果は下記のとおりです。

期待通りの修正結果となりました。
ソート仕様の修正
コードを実行したところ、氏名のリストのソートが降順になっていたため、昇順でソートするように変更を要求してみます。

修正を受け入れ、再度コードを実行したところ、昇順でソートされました。
コメントの追加
サンプルコードのコメントがすべて削除されたため、リファクタリングしたソースに対してコメントを追加できるかを確認します。
「コメントを生成してください。」と入力すると下記のような状態になりました。

ほぼ満点なコメントを生成してくれました。
コメントを書かなかった既存ソースが世の中には多くある思います。コメントを自動補完できれば、大変助かりますね。
まとめ
Cursorを使用して新規開発を行う場合、業務的なロジックの作成等にはハードルが高いかもしれませんが、既存のソースコードに対して、リストの差分取得やソースコードのリファクタリングなど煩雑なロジックをAIに任せると、開発が楽になると思います。
実際の開発現場では、セキュリティの都合でAIを使用した開発ができないことも多いと思いますが、使用できれば生産性の向上に大きく寄与できると感じました。
リファクタリングされたソース
package com.test.cusor;
/**
* 人物情報を表すクラス
*/
public class Person {
/** 人物の名前 */
private final String name;
/** 人物の年齢 */
private final int age;
/**
* コンストラクタ
* @param name 人物の名前
* @param age 人物の年齢
*/
public Person(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 名前を取得する
* @return 人物の名前
*/
public String getName() {
return name;
}
/**
* 年齢を取得する
* @return 人物の年齢
*/
public int getAge() {
return age;
}
/**
* 人物情報の文字列表現を返す
* @return 人物情報の文字列
*/
@Override
public String toString() {
return String.format("Person{name='%s', age=%d}", name, age);
}
}
package com.test.cusor;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 人物情報を管理するメインクラス
*/
public class Main {
/** 対象となる人物の名前リスト */
private static final String[] TARGET_NAMES = {"山田太郎", "渡辺美咲", "加藤翔", "小林優子", "田中愛子"};
/** 全人物の名前リスト */
private static final String[] ALL_NAMES = {
"山田太郎", "佐藤花子", "鈴木一郎", "高橋健太", "田中愛子",
"伊藤誠", "渡辺美咲", "中村拓也", "小林優子", "加藤翔"
};
/** 年齢の最小値 */
private static final int MIN_AGE = 20;
/** 年齢の最大値 */
private static final int MAX_AGE = 79;
/**
* メインメソッド
* 1. 人物リストを生成
* 2. 対象人物をフィルタリング
* 3. 年齢でソート
* 4. 結果を表示
*/
public static void main(String[] args) {
List<Person> persons = getPersons();
List<Person> filteredPersons = filterPersonsByName(persons, TARGET_NAMES);
List<Person> sortedPersons = sortPersonsByAge(filteredPersons);
System.out.println(sortedPersons);
}
/**
* 人物リストを生成する
* @return ランダムな年齢を持つ人物のリスト
*/
private static List<Person> getPersons() {
Random random = new Random();
return Arrays.stream(ALL_NAMES)
.map(name -> new Person(
name,
random.nextInt(MAX_AGE - MIN_AGE + 1) + MIN_AGE
))
.collect(Collectors.toList());
}
/**
* 指定された名前の人物のみをフィルタリングする
* @param persons フィルタリング対象の人物リスト
* @param targetNames 対象となる名前の配列
* @return フィルタリングされた人物リスト
*/
private static List<Person> filterPersonsByName(List<Person> persons, String[] targetNames) {
Set<String> targetNameSet = new HashSet<>(Arrays.asList(targetNames));
return persons.stream()
.filter(person -> targetNameSet.contains(person.getName()))
.collect(Collectors.toList());
}
/**
* 人物リストを年齢の昇順でソートする
* @param persons ソート対象の人物リスト
* @return 年齢の昇順でソートされた人物リスト
*/
private static List<Person> sortPersonsByAge(List<Person> persons) {
return persons.stream()
.sorted(Comparator.comparingInt(Person::getAge))
.collect(Collectors.toList());
}
}