背景
昔テレビで「床屋へ一日に何回も行くとどうなるのか」という企画を見たことがあり
それが元ネタ。AIの反復自動改善がどんな帰結を迎えるのか、興味本位で確かめてみた。
実験設計
対象コードはJavaで作ったFizzBuzz。
髪の毛が伸びて清潔感がない状態を表現して、意図的に処理を汚した状態でスタート。
public class FizzBuzz {
public static void main(String[] args) {
for (int i=1;i<=100;i++){
if (i%3==0 && i%5==0){
System.out.println("FizzBuzz");
} else {
if (i%3==0){
System.out.println("Fizz");
} else {
if (i%5==0){
System.out.println("Buzz");
} else {
System.out.println(i);
}
}
}
}
}
}
リファクタリング用プロンプト
途中からバリエーションを入れると結果が変わるので、固定文言を投げ続ける
(英語のほうがブレが少ない)
Refactor this Java code to improve readability, maintainability, and testability.
Do not change its external behavior. Return only the code block.
日本語訳
このJavaコードをリファクタリングし、読みやすさ、保守性、テスト可能性を向上させてください。
外部動作を変更しないでください。コードブロックのみを返してください。
検証
Node.js + Gemini API (gemini-2.5-flash-lite) を利用
ループで1回前の出力を次回の入力にそのまま渡して、各実行結果をまとめてファイルに出力。
出力結果はGitへアップしてます。
java-result
結果
なんとなく想像はついてましたが1回目で長髪から短髪になるくらい改変されました。
// 1回目
public class FizzBuzz {
private static final String FIZZ = "Fizz";
private static final String BUZZ = "Buzz";
private static final String FIZZ_BUZZ = FIZZ + BUZZ;
private static final int FIZZ_DIVISOR = 3;
private static final int BUZZ_DIVISOR = 5;
private static final int MAX_NUMBER = 100;
public static void main(String[] args) {
for (int i = 1; i <= MAX_NUMBER; i++) {
System.out.println(generateFizzBuzz(i));
}
}
/**
* Generates the FizzBuzz output for a given number.
*
* @param number The number to process.
* @return "FizzBuzz" if the number is divisible by both 3 and 5,
* "Fizz" if divisible by 3,
* "Buzz" if divisible by 5,
* otherwise the number itself as a String.
*/
public static String generateFizzBuzz(int number) {
boolean isDivisibleByFizz = isDivisibleBy(number, FIZZ_DIVISOR);
boolean isDivisibleByBuzz = isDivisibleBy(number, BUZZ_DIVISOR);
if (isDivisibleByFizz && isDivisibleByBuzz) {
return FIZZ_BUZZ;
} else if (isDivisibleByFizz) {
return FIZZ;
} else if (isDivisibleByBuzz) {
return BUZZ;
} else {
return String.valueOf(number);
}
}
/**
* Checks if a number is divisible by a given divisor.
*
* @param number The number to check.
* @param divisor The divisor.
* @return true if the number is divisible by the divisor, false otherwise.
*/
private static boolean isDivisibleBy(int number, int divisor) {
return number % divisor == 0;
}
}
マジックナンバーが定数化して、メソッド分割がされてます。
docも追加されて保守性も上がってますね。
この時点で実務コードとしては十分読めるレベルです。
// 2回目
public static void main(String[] args) {
printFizzBuzzUpTo(DEFAULT_MAX_NUMBER);
}
/**
* Prints the FizzBuzz sequence for numbers from 1 up to the specified maximum number.
*
* @param maxNumber The upper limit of the sequence (inclusive).
*/
public static void printFizzBuzzUpTo(int maxNumber) {
for (int i = 1; i <= maxNumber; i++) {
System.out.println(generateFizzBuzzOutput(i));
}
}
2回目では責務分割としてロジック生成と出力が分離されました。
エントリーポイントがより宣言的になっています。
以降は特に大きな変化なし
基本的には判定ロジックのバリエーションが切り替わるだけでした。
共通関数方式
private static boolean isDivisibleByBoth(int n) {
return isDivisibleBy(n, FIZZ_DIVISOR) && isDivisibleBy(n, BUZZ_DIVISOR);
}
...
if (isDivisibleByBoth(number)) return FIZZ_BUZZ;
15倍数ショートカット
if (isDivisibleBy(number, FIZZ_DIVISOR * BUZZ_DIVISOR)) {
return FIZZ_BUZZ;
}
ふつうの&&判定
if (isDivisibleBy(number, FIZZ_DIVISOR) && isDivisibleBy(number, BUZZ_DIVISOR)) {
return FIZZ_BUZZ;
}
基本この3パターンのシャッフルが延々と続いて機能差はなし、
可読性もほぼ同等に思えます、違いを作ることだけが目的化した状態なのかもしれません。
あとは名前変更程度。
まとめ
概ね、予想はついていましたが
小規模のスクリプトは2〜3回でリファクタリングが打ち止め状態になり
以降は差分を作るための差分になりがちのようです。
大胆な設計変更とかが発生すれば、面白いなと思っていたのですが、
もっと色々な組み方が可能なテストコードにすればよかったかも?
別のソースコードでも試してみたいですね。