生成AIで実装スピードは上がりました。(自分は入社時からAIを使っているので前時代のことはよくわかりませんが...)
なのに、体感としては
- 実装:速い(PRができる)
- でも レビュー:指摘が多い
- さらに デバッグ:時間が溶ける
- 結果、トータルは遅い
実際に、AI支援の変更はPR上の問題(issues)が増えやすいというレポートも出ています。 (CodeRabbit)
また、LLM生成コードは参照実装よりコードスメルの発生が増える、という研究報告もあります。 (arXiv)
この記事はその「遅くなる原因」を、“AIっぽい匂い”として嗅ぎ分けて直すための自分の経験をまとめたカタログです!
対象読者
- いわゆる vibe coding で実装を進めている(Cursor / Copilot / Claude Code 等)
- 「動くけど、PRで揉める」「直すのが怖い」が増えてきた
- 既存コードへの“馴染ませ方”を言語化したい
なぜAIだと“匂い”が増えるのか(ざっくり)
生成AIは「今この場で動く解」を出すのが得意です。
でも開発のボトルネックは、だんだん “書く”より“読む/直す/揃える” に寄ります。
- 読む:理解できないとレビューが止まる
- 直す:影響範囲が読めないとデバッグが沼る
- 揃える:既存の流儀から浮くと、説明コストが増える
つまり「生成は速い」が「理解は別ゲーム」。
このギャップが“匂い”として現れます。
AIコードの匂いカタログ(厳選5本)+直し方
それぞれ「サイン / 何が困る / 直し方(具体)」でまとめます。
1) 既存コードとの一貫性が壊れている
サイン
- 命名が突然別文化(
UserManagerが急に生える、fetch/get/loadが混在) - エラーの返し方がそのファイルだけ違う(例外派 vs Result派)
- 置き場所・責務の切り方がその変更だけ浮いてる
何が困る
- 読む側が「方言の翻訳」を強いられてレビューが遅くなる
- “なぜこの形?”の説明が必要になり、PRが会話地獄になる
直し方(具体)
(A) まず既存の「型」を観察して寄せる
- 命名:同じ責務のクラス/関数を3つ探して、単語を揃える
- 例外:同じ層の関数が「throwしてるか / Result返してるか」を揃える
- ファイル配置:同種の処理がどこにあるかに寄せる
(B) “差”には理由をつける
- 既存と違う形を採るなら、PR本文に「なぜ必要か」を短く書く
(理由のない差分は、匂い扱いされがち)
この「一貫性を守る」は、AI時代ほど効きます。AIの出力はそれっぽくても、リポジトリの流儀までは自動で揃いきらないからです。
2) レイヤー(MVC等)の境界が溶ける/外部依存が染み出す
サイン
- ControllerからDB/外部API直叩き、ViewModelがドメインルール持つ
- ドメイン層にORM/HTTPの型が露出する
- 「同じルール」が複数層にコピペされる(バリデーション地獄)
何が困る
- 仕様変更が“増幅”する(直す箇所が芋づる式に増える)
- 影響範囲が読めず、デバッグで迷子になる
直し方(具体)
(A) “知識の置き場所”を固定する(情報隠蔽の発想)
- 「そのルール/形式を誰が知るべきか?」を決めて、そこに閉じ込める
例:HTTPの詳細は境界(Controller/Adapter)まで、ドメインに持ち込まない
(B) 境界に“変換レイヤー”を置く
- 外部API → 自前DTO → ドメインモデル のように、型の越境を1箇所に集約
- ドメイン側は「自分の言葉(型)」だけで喋らせる
この発想は「モジュールは深く(インターフェースは薄く)」にも繋がります。 (ソフトウェア工学の現代的アプローチ)
3) 分岐迷路(深いネスト / if-swich増殖 / フラグ引数)が同時発生する
サイン
-
ifが深い、ハッピーパスが見えない -
switchが肥大化し、追加のたびに同じ塊を編集 -
render(true)みたいなフラグ引数で内部が二股三股
何が困る
- 目視で追える条件の限界を超える(レビュー・デバッグが止まる)
- “追加が怖い”状態になり、保守が詰む
直し方(具体)
(A) ネストはガード節で平坦化
- 「例外/早期returnを先に並べて、通常系を一番見える場所に置く」
これは定番のリファクタリングとして整理されています。 (リファクタリング guru)
例(擬似コード):
// before
function price(user) {
if (user) {
if (user.plan) {
if (user.plan.type === "pro") { ... }
else { ... }
} else { ... }
} else { ... }
}
// after(ガード節)
function price(user) {
if (!user) return 0
if (!user.plan) return 0
if (user.plan.type === "pro") return proPrice(user)
return basicPrice(user)
}
(B) switch/if増殖は“分岐の理由”を型に移す
- 「条件分岐で行動が変わる」なら多態へ
Replace Conditional with Polymorphism(代表例) (リファクタリング guru)
(C) フラグ引数は関数を分けて“意図を名前へ”
-
render(true)→renderForSuite()/renderForSingle()のように
「呼び出し側が意味を読める」形にする
(D) 引数が増えるなら “概念の塊” にまとめる
- Introduce Parameter Object(いつも一緒に動く引数を1つの型へ) (リファクタリング guru)
4) 追跡が長い(Trainwreck / パススルー / Shallow Module)
サイン
-
これ系が増える:
a.getB().getC().getD()- 「AがBを呼ぶだけ」「引数を渡すだけ」の中継が多い
-
変更の影響を見るために、ファイル間を往復する回数が増える
何が困る
- 読むコストが増えるだけで、理解が進まない(=レビューが重い)
- 内部構造に依存して壊れやすい(=デバッグがしんどい)
直し方(具体)
(A) Tell, Don’t Ask:データを聞いて外で処理しない
- 「値を取り出して組み立てる」のではなく、「オブジェクトにやらせる」 (martinfowler.com)
// before(聞いて外で処理)
const path = ctx.getOptions().getScratchDir().getAbsolutePath()
// after(命じる)
const path = ctx.scratchPath()
(B) デメテルの法則:知りすぎない
- メソッドチェーン(Trainwreck)を匂いとして扱う考え方 (Qiita)
(C) “浅い層”を減らして、モジュールを深くする
- 使い手が考えることを減らし、複雑さをモジュール内部に押し込む(Deep Module) (ソフトウェア工学の現代的アプローチ)
5) 意味が型に乗らない(Primitive Obsession / 汎用コンテナ / マジック値)
サイン
-
string/intに意味が詰め込まれている(単位・制約が不明) -
Map<String, String>やPairが返ってきてget("x")祭り -
-1やnullが“特別な意味”を持っている
何が困る
- 取り違えがコンパイルもテストもすり抜ける
- 呼び出し側が暗黙ルールを覚える必要があり、レビューが荒れる
直し方(具体)
(A) 値オブジェクト化:意味・制約・単位を閉じ込める
- Replace Data Value with Object(代表例) (リファクタリング guru)
// before
function charge(amount: number) { ... } // 円?ドル?税抜?
// after
class Money { constructor(readonly yen: number) { ... } }
function charge(amount: Money) { ... }
(B) 汎用コンテナは “名前のある戻り値” にする
-
Pair/Mapを返すより、専用DTOでフィールド名を与える
→ 読む側が「意味」を推測しなくて済む
(C) マジック値は型で表す
- Optional / Result 型、もしくは例外方針の統一(※項目1とセットで効く)
まとめ:AIの出力は「原稿」。速さを成果に変えるのは「編集」
vibe coding が強いのは、原稿(動くコード)を出す速度です。
でもチーム開発で効くのは 読めること・揃っていること・境界が守られていること。
今回の5つは、AIコードの違和感が出やすい場所を“圧縮”したものです。
- 一貫性(既存の文脈に馴染ませる)
- 境界(レイヤーを守る)
- 分岐(迷路にしない)
- 追跡(往復を減らす)
- 型(意味を閉じ込める)
参考
- CodeRabbit「AI vs Human code generation report」(AI PRはissuesが増えやすい) (CodeRabbit)
- LLM生成コードのコードスメル増加を報告する研究 (arXiv)
- Tell, Don’t Ask(Martin Fowler) (martinfowler.com)
- Replace Nested Conditional with Guard Clauses / Introduce Parameter Object(Refactoring.Guru) (リファクタリング ガuru)
- Deep Modules(Ousterhoutの整理の紹介) (ソフトウェア工学の現代的アプローチ)
- Qiitaで読まれる書き方(導入で読む理由を約束、フォーマットで読むストレスを減らす等) (Qiita)