最近はAIを使ってコードを書く機会がかなり増えてきました。
自分も日常的にAIを活用していますが、開発速度が上がった一方で、コードレビューの難しさは以前より増しているように感じています。
というのも、AIが生成するコードは一見すると非常によくできているからです。
命名も自然で、構文エラーも少なく、ちょっとした処理であればそのまま動いてしまうことも珍しくありません。
ただ、コードとして正しいことと、期待どおりに動作することは別の話です。
今回はKotlinの簡単なコードを例に、AIが生成したコードをレビューする際に、どのような点を意識するべきなのか整理してみます。
なお、掲載するコードは説明のために作成したサンプルです。
正常系だけなら、それらしいコードは簡単に書ける
例えば、2つの日付の間に何日あるのかを計算する処理を考えてみます。
fun calculateDaysBetween(
startDate: LocalDate,
endDate: LocalDate
): Long {
return ChronoUnit.DAYS.between(startDate, endDate)
}
非常にシンプルな実装です。
val startDate = LocalDate.of(2026, 10, 1)
val endDate = LocalDate.of(2026, 10, 10)
println(calculateDaysBetween(startDate, endDate))
// 9
ビルドも通りますし、実行結果も正しく見えます。
しかし、この実装があらゆるケースで期待どおりに動作するかというと、そうとは限りません。
例えば、開始日と終了日が逆になった場合はどうでしょうか。
val startDate = LocalDate.of(2026, 10, 10)
val endDate = LocalDate.of(2026, 10, 1)
println(calculateDaysBetween(startDate, endDate))
// -9
ChronoUnit.DAYS.between()は、開始日から終了日までの差を符号付きで返すため、結果は負の値になります。
これはAPIとしては正しい動作です。
しかし、仮に「2つの日付の間隔を必ず正の値で返す」という仕様だった場合、この実装は期待する動作を満たしていません。
逆に、「終了日が開始日より前の場合はエラーにする」という仕様なら、絶対値を返す実装も適切ではありません。
つまり、どの実装が正しいのかは、仕様によって変わるわけです。
AIに「2つの日付の差を計算する関数を作って」と依頼すれば、おそらく簡単にコードを生成してくれるでしょう。
ただし、日付の前後関係をどう扱うべきかまで、こちらが期待しているとは限りません。
AIが正しいコードを書いていても、こちらが求めている処理とは限らない。
まずはこの違いを意識しておく必要があると思います。
動いているからこそ、気付きにくい問題もある
もう少し別の例を見てみます。
整数のリストから重複を取り除き、小さい順に並び替える処理です。
fun normalizeNumbers(numbers: List<Int>): List<Int> {
return numbers.distinct().sorted()
}
こちらも特に問題があるようには見えません。
val numbers = listOf(5, 3, 1, 3, 5, 2)
println(normalizeNumbers(numbers))
// [1, 2, 3, 5]
期待どおりの結果になっています。
では、仮にこの関数に求められている仕様が「重複を取り除き、元の順番を維持する」だったらどうでしょうか。
期待する結果は次のようになります。
// [5, 3, 1, 2]
この場合、sorted()は不要です。
fun normalizeNumbers(numbers: List<Int>): List<Int> {
return numbers.distinct()
}
Kotlinのdistinct()は、元のリストで最初に出現した要素の順序を保持します。
そのため、こちらの実装が適切になります。
では、なぜ最初のコードにsorted()が追加されてしまったのでしょうか。
例えば、AIに次のような依頼をした場合です。
数値のリストを整理する関数を作ってください。重複する値は除外してください。
「整理する」という表現から、AIが並び替えまで必要だと解釈する可能性があります。
そして、生成されたコード自体は正常に動作します。
ビルドも通りますし、例外も発生しません。
それでも、仕様からは外れてしまっています。
このような問題は、実装だけを見ていても気付きにくいものです。
コードが何をしているのか理解することはもちろん重要ですが、それ以上に、その処理が本当に必要なのかを確認することが大切だと思います。
テストが通っていても安心できない
ここまでの例を見ると、テストを書けば解決するのでは、と思うかもしれません。
もちろん、テストは非常に重要です。
ただし、テストが通っていることも、実装の正しさを完全に保証するものではありません。
例えば、先ほどの日付計算を考えてみます。
@Test
fun `日付の差を正しく計算できる`() {
val startDate = LocalDate.of(2026, 10, 1)
val endDate = LocalDate.of(2026, 10, 10)
val result = calculateDaysBetween(startDate, endDate)
assertEquals(9L, result)
}
このテストは成功します。
しかし、確認しているのは開始日が終了日より前のケースだけです。
次のようなケースについては確認できていません。
- 開始日と終了日が同じ場合
- 開始日が終了日より後の場合
- 月をまたぐ場合
- 年をまたぐ場合
- うるう年を含む場合
なお、今回使用しているChronoUnit.DAYS.between()自体は、LocalDateに対してこれらの日付差を計算できます。
問題なのはAPIの実装ではなく、アプリケーションとして期待する動作をテストできているかという点です。
例えば、終了日が開始日より前の場合はエラーとする仕様なら、次のようなテストが必要になります。
@Test
fun `終了日が開始日より前の場合はエラーになる`() {
val startDate = LocalDate.of(2026, 10, 10)
val endDate = LocalDate.of(2026, 10, 1)
assertThrows<IllegalArgumentException> {
calculateDaysBetween(startDate, endDate)
}
}
そして、その仕様を満たす実装は次のようになります。
fun calculateDaysBetween(
startDate: LocalDate,
endDate: LocalDate
): Long {
require(!endDate.isBefore(startDate)) {
"終了日は開始日以降である必要があります"
}
return ChronoUnit.DAYS.between(startDate, endDate)
}
こうして仕様が明確になっていれば、実装とテストの両方で期待する動作を確認できます。
一方で、仕様が曖昧な状態でAIにコードとテストをまとめて生成させた場合、同じ誤った解釈に基づいて両方が作られる可能性もあります。
その結果、テストは通っているのに、求めている動作にはなっていないという状況が起こり得ます。
テストの実行結果だけでなく、何を検証しているのかまで確認する必要があります。
AIにレビューさせれば解決するのか?
AIが生成したコードを、別のAIにレビューさせる方法もあります。
自分もAIによるコードレビューは有効だと考えています。
ただ、それだけで十分かと言われると、少し疑問が残ります。
例えば、先ほどのコードをそのまま渡してみます。
以下のコードに問題がないかレビューしてください。
fun normalizeNumbers(numbers: List<Int>): List<Int> {
return numbers.distinct().sorted()
}
このコードだけを見た場合、distinct()で重複を取り除き、sorted()で昇順に並び替えていることは分かります。
しかし、並び替えが必要かどうかは判断できません。
そのため、レビューを依頼するのであれば、実装だけでなく仕様も合わせて伝える必要があります。
以下の仕様を前提にコードをレビューしてください。
仕様:
・リスト内の重複する値を取り除く
・元の要素の並び順は維持する
・空のリストが渡された場合は空のリストを返す
・元のリストは変更しない
確認したいこと:
・仕様と異なる動作になる箇所
・不要な処理が含まれていないか
・境界値や特殊な入力で問題がないか
・コードだけでは判断できない点
問題がない場合も、その理由を説明してください。
こうすることで、単なるコードの品質ではなく、仕様との整合性を確認しやすくなります。
個人的には、最後の「コードだけでは判断できない点」を挙げてもらうことも重要だと思っています。
AIは情報が不足していても、それらしい回答を生成できてしまいます。
だからこそ、不明な点を無理に補完させず、何が分からないのかを明確にすることが大切です。
AIがコードを書く時代に、何をレビューするべきなのか
AIの活用によって、コードを書く時間は以前より短縮しやすくなりました。
一方で、コードが正しく動作することを確認するための作業が、同じ割合で減るとは限りません。
特に意識したいのは、次のような部分です。
1. 仕様を満たしているか
コードとして正しくても、期待する動作と異なっていれば意味がありません。
AIがどのような前提で実装したのかを確認する必要があります。
2. 不要な処理が含まれていないか
AIが気を利かせて追加した処理が、必ずしも必要とは限りません。
今回のsorted()のように、処理自体は正しくても、仕様上は不要なケースがあります。
3. 正常系以外も考慮されているか
正常な入力で期待どおりに動作することは、最低限の確認です。
空のリスト、想定外の値、境界値などについても考慮する必要があります。
4. テストが仕様を検証しているか
テストが通ることだけでなく、テストケースそのものが適切なのかを確認します。
AIが生成したコードとテストが、同じ誤った前提に基づいていないかも注意したいところです。
5. 実装の意図を説明できるか
最終的には、なぜその実装になっているのかを理解できていることが重要だと思います。
AIが生成したからという理由だけで採用するのではなく、少なくとも主要な処理については、自分の言葉で説明できる状態にしておきたいです。
最後に
AIを使った開発には、今後も積極的に取り組んでいきたいと考えています。
実装にかかる時間を減らし、その分を設計や検証に使えるようになるのは、大きなメリットです。
ただ、AIが生成したコードを理解しないまま採用してしまうと、問題が発生したときに原因を追うことも難しくなります。
また、AIが指摘しなかったから問題ない、という判断も避けたいところです。
今回取り上げた例は、どれも特別に難しい処理ではありません。
日付の差を計算する、リストの重複を取り除く。
それだけの処理でも、仕様の解釈が少し違えば、期待する結果とは異なってしまいます。
AIがコードを書けるようになったからこそ、そのコードがなぜ正しいのかを説明できることが重要になる。
少なくとも現時点では、そう考えています。
コードを書くことだけに時間を使うのではなく、何を作るべきなのか、どうすれば正しいと判断できるのか。
そういった部分にも、これまで以上に目を向けていきたいですね。