初めに
初めまして!エンジニアを目指して奮闘している者です!
今回はAIを活用して作品を制作を行った際の失敗談を語っていけたらなと思います。
現在3つ作品(1つはデプロイ経験用)を完成させ、GitHubに投稿しています。
もしよろしければ今回の記事はAIに関して未経験からの視点っていう事で今回の失敗談をご覧ください!
良かった点
失敗談だけでは後味が悪いので良かった点も載せておきます!
自分の実力以上の作品が作れる
- コードを知らなくてもやりたい事や処理を的確に言語化することが出来れば、ちゃんと動くコードを出力してくれます。これで複雑な処理の実装をAIに任せて自分は設計や挙動の思考に専念することが出来ます。
開発効率が上がる
-
自分でコードを書いたり、開発環境を整えるのに調べて解決するという手間が大幅に削減されます。
-
今までは開発環境を整えたり、実装したいけど詰まったときはググって他のコードを参考にしたりしてました。(AI時代からエンジニアを目指したのでほんとに尊敬します)
- 動かない原因がミスタイプ等、
ヒューマンエラーに気づくまでに数時間、数日かかるという事が解消されます。 - エラーログを投げればたいてい解決できますし、タイポミスは人間が見つけるより機械のほうが得意ですから、タイポミス等で動かない状況は特に改善されているのではないのかと思ってます。
- 動かない原因がミスタイプ等、
分からないことは何でも聞ける
-
この処理は何を行っているのかやなぜ動かなかったのかを詳しく教えてくれます。
メンター的な扱いが出来るので効率よく知識を増やすことが可能です。 -
また、動くけど他の最適なコード例を出してもらえたりするので上手く活用すれば全体的にレベルの高いコードを書くことが可能で、自分自身も引き出しを増やすことで最適なコードを選出することが出来ます。
失敗談
今はAIの使い方を変えてますが、LPを完成させた時までの失敗談を語っていきます。
知識が無く、殆どコピペをつなぎ合わせたコードで完成させたため、いざバグ修正やリファクタリング(改善)をしようとするとどこで何をしてるのか全く分からない状態になりました。
おそらく失敗談と思われるほとんどのパターンがこれだと思ってます。
実際に私がAIに書かせた失敗コード(長すぎるので一部コメントで省略しています)を見てみましょう。
今回提示するコードは JavaScript となります。
function handleKey(k) {
// 1. 連打・過剰入力チェック(ここはそのまま)
if (segments.length > 0 && segments.every(s => s.isDone())) {
const allGreen = segments.every(s => s.typedLog.every(t => t.color === "#4aff50"));
if (!allGreen) {
// ペナルティの計算など
// UIの更新など
const wordEl = document.getElementById("text-word");
if (wordEl) {
// エラーシェイクのアニメーション
wordEl.classList.remove("error-shake");
void wordEl.offsetWidth; // 強制リフローさせることが出来ます。
wordEl.classList.add("error-shake");
}
}
return;
}
// 音を鳴らす処理
// 記号変換
let inputChar = k;
// ,.押されたときの句読点変換処理 、と。
// =========================================================
// ★修正ポイント:n連打(n -> nn)の特例処理
// =========================================================
// 条件:
// 1. 入力が 'n' である
// 2. 直前に打ったセグメントがある (segIndex > 0)
if (inputChar === 'n' && segIndex > 0) {
const prevSeg = segments[segIndex - 1];
// 「今のセグメント(seg)」が、この 'n' を正解として受け入れるか確認
// 例: 次が "na" なら 'n' は正解なので、書き換え処理はしない
const isCorrectForCurrent = seg.patterns.some(p => p.startsWith(seg.inputBuffer + inputChar));
// ★「今のセグメントでは不正解(ミス)」かつ「前のセグメントが拡張可能」な時だけ発動
// 3. 直前のセグメントが「ん(n)」で、かつ「n」1文字で終わっている状態である
if (!isCorrectForCurrent && prevSeg.canonical === 'n' && prevSeg.inputBuffer === 'n') {
console.log("救済措置: n -> nn に拡張します");
// 1. 前のセグメントを 'nn' に書き換える
// ★追加:拡張フラグを立てる(BS一発消し用)
prevSeg.isExpanded = true;
// 2. 正解扱いとしてボーナス加算処理
// スコア加算
// 3. 画面更新
updateRankUI(); // ランク更新
render(); // 画面再描画
// ★★★ 最重要:ここで関数を強制終了! ★★★
// これがないと、下の「通常の判定処理」に進んでしまい、
// 「g に対して n を打った」というミス判定が二重に発生します。
return;
}
}
// --- 通常の判定処理 (ここから下は変更なし) ---
// 2. 正解のとき
// 正解の処理、UI更新等
let multiplier = getScoreMultiplier();
createScorePopup(addScore);
updateRankUI();
if (gaugeValue >= currentGaugeMax) {
// 連打ゲージがたまった処理
}
}
// UI更新関数
}
// 3. ミスのとき
else if (result.startsWith("MISS")) {
// ゲージを減らしてミスの数を増やし、
// リザルトのためにミスした単語とローマ字を記憶する処理;
// 上記の記憶したミスが被ったら加算する
// UI更新関数
}
// ミス演出
if (result.startsWith("MISS")) {
// ミス時の処理UI更新等
if (wordEl) {
// エラーシェイクのアニメーション
wordEl.classList.remove("error-shake");
void wordEl.offsetWidth; // 強制リフローさせることが出来ます。
wordEl.classList.add("error-shake");
}
}
// 次へ進む
// 周りの虹色演出を解除
// すべて完了しているかチェック
// 全て緑色(ミスなし)かチェック
// パーフェクト(赤文字なし & ミスカウント0)ならボーナス加算
// ボーナスのポップアップ
createScorePopup(romaBonus);
}
}
// 完了チェック
render();
const isJustMissed = (result.startsWith("MISS"));
checkCompleted(isJustMissed);
}
だいぶコメントでごまかしましたが全体はこの関数だけで約200行あります💦
何が行けないのかはもうお分かりだと思いますが何がダメなのか一部を抜粋して紹介します。
役割が多すぎる(GOD関数)
この関数名、キー入力のチェックをするのかな?と思いきや想像がつかないほどの役割が与えられています。
実際にこの関数で行っていることはhandleKey(キー入力の処理) なのにも関わらず、その中で「音を鳴らす」「正解時とミス時の処理」「スコアを計算する」「DOM(HTML)を直接書き換える」「コンボを計算する」「UIを更新する」という、ゲームの全権を握る処理が行われています
このような関数を 神関数(God Object) といいます。
これの何がやばいかというと、アニメーションを変えたいだけなのに他の場所がバグを起こす(デグレといいます)リスクがあり、コードを修正するのがとても大変になります。
対策案:関心の分離(単一責任の原則)
関数やファイルごとに 「1つのことしかしない」 ように役割を分割(関心の分離)します。
-
judgeTyping(key): 入力が合っているか「判定」するだけの係 -
updateScore(result): 判定結果をもとに「スコア計算」だけをする係 -
playEffect(result): 判定結果をもとに「音やUI演出」だけをする係などなど
このように、handleKey はそれぞれの係に 「指示を出すだけの司令塔」 に徹するべきでした。
いたるところで関数が呼ばれ、処理の流れを追うのが大変
-
上記の神関数に加え、様々な分岐条件の奥深くで
updateRankUI()やrender()といった画面更新の関数が唐突に呼ばれています。 -
「キーを押す ➔ 判定する ➔ なぜかその途中で画面が再描画される ➔ また別の判定が走る」という複雑な
依存関係が生まれており、どこでどのタイミングで画面が切り替わっているのか、作った本人ですら数日後には完全に迷子になります。(コメントが無いと特に -
このような状態を密結合と良い、他の関数に依存したりしてるため、一箇所バグるだけで原因の特定が難しく、またテストコードも書きづらくなります。
対策案:状態(State)と描画(View)を分ける
ロジックの途中で都度 render() を呼ぶのではなく、「データの更新」がすべて終わった一番最後に、1回だけ画面を描画するようにします。
-
MVC(Model-View-Controller)というアーキテクチャ設計の考え方があります-
Modelは主にデータ処理やロジック、Viewは画面表示やUI更新等、Controllerは入力(ユーザーからのリクエスト)を受け取り(仲介役)、Modelに処理を依頼、最後にViewで画面更新を指示といった役割があります
-
この考え方を適応することで適切なロジックを分けが可能になり、保守性が向上するので開発を進めやすくなります。単純な処理ならともかく規模が大きくなってきたらMVCを意識するといいかもです!
マジックナンバーだらけ
コメントで隠れていますが実際は計算処理の中に300や5や#4a5500等、至る所に書いてあります。これをマジックナンバーと呼びます。
- 何がダメなのか
- 例えば正解のスコアが
100でこれを200上げたいなってなりました。そしたら正解の100という数字を片っ端から変えないといけません。 - もしかしたら別ファイルでも参照されてる可能性があるため、見落としてしまうとバグってしまう可能性もあります。
-
100という数字を全部置換すればいいじゃんと思いますが別の処理で100が使われていた時に誤って置換してしまい、ロジックが壊れる可能性もあるためオススメしません。
- 例えば正解のスコアが
対策案
関数の外や別フォルダに定数ファイルを作成し、設定値として名前を付けて管理します。
const CONFIG = {
PENALTY_SCORE: 300, // 減点スコア
COLOR_CORRECT: "#4aff50" // 正解時の色
};
// 使う時は CONFIG.PENALTY_SCORE とする、または
const { PENALTY_SCORE, COLOR_CORRECT } = CONFIG;
// と展開することで個々の変数として定義することが出来る。
// これを分割代入といいます(使い方は、PENALTY_SCOREでCONFIG.を省略できる)
// やりすぎても何の変数か分かりづらくなるため適切に
// 何度も使うなら分割、一回しか使わないならCONFIG.のまま等
-
こうすることでバランス調整したいなってなったときに定数の値をいじるだけで全体に反映されます。
-
フォルダとファイル内の分け方としましては、自分は他にも参照される場合は
定数フォルダに、ファイル内で完結する場合はファイル内で分けてます。 -
マジックナンバーは修正や意識で一番取り組みやすいかと思います。
共通関数に出来る箇所だらけ(DRY原則の無視)
- 何がダメなのか
- ミスをした時の「画面を揺らす処理(エラーシェイク)」や「スコアのポップアップ処理」が、まったく同じコードで上下に2回以上書かれていました。
- プログラミングには 「DRY(Don't Repeat Yourself = 繰り返しを避ける)原則」 という原則があります。同じコードが複数あると、仕様変更の際に「片方は直したけど、もう片方を直し忘れた」という
致命的なヒューマンエラーを誘発します。
対策案: 共通処理のヘルパー関数化
よく使う処理は、独立した 小さな関数(ヘルパー関数) として外に切り出します。
// エラー時の演出を処理する共通関数(アロー関数でも書けます)
const triggerErrorShake = (el) => {
el.classList.remove("error-shake");
void el.offsetWidth; // 強制リフローさせることが出来ます。
el.classList.add("error-shake");
}
if (wordEl) {
// ここで呼び出すだけ
triggerErrorShake(wordEl);
}
-
こうしておけば、どこでミスが起きても
triggerErrorShake(el)と1行書くだけで使い回せるようになり、コードが劇的にスッキリします。 -
void element.offsetWidth;はブラウザにレイアウトを計算しなおせと命令することが出来ます。変更を即座に反映させたいときに使われる強制同期レイアウトです。
ただリフロー(再描画)はブラウザの処理負荷が高いので、やりすぎには注意です。
後はエラーハンドリングが全くされておらず、エッジケースを考慮されていないため、例外処理が発生した際にアプリがクラッシュしてしまい、ユーザーが困惑するというUXの低下と修正の際のバグの追求の難化、ハードコーディングされていたりと知識が無いとこれらを見分けることが出来ず、脆弱性と堅牢性皆無のコードが出来上がります。
ハードコーディングとは?
URLやAPIキーなどを、ソースコードの中に直接ベタ書きしてしまう状態のことです。
これをやってしまうと、以下の2つの大きな問題が発生します。
- セキュリティの危険性: APIキーを書いたままGitHub等に公開すると、Botに数秒で検知され、悪用(高額請求など)される危険があります。
- 保守性の低下: 開発環境(ローカル)と本番環境でURLが違う場合、デプロイのたびにコードを書き直すハメになります。
そのため、機密情報や環境によって変わる値はコードに直接書かず、環境変数(.envファイルなど) に切り出して管理するのが開発の鉄則です。
-
知識のある人がAIを使うと効率が爆増し、品質の高い作品が出来上がりますが、知識がないとバグだらけの品質の低い作品が出来上がり、技術負債だらけのコードとなってかえって効率が悪くなると思っております。
-
自分もまだ知識をつけている段階で完全に改善はしきれておりませんが、機能追加どころではないくらい負債だらけのコードが出来上がっており、一通りましになったなーくらいまでに書き直すのに時間がかかりました。
-
AIは使う人のレベルに合わせてコードを出すので、知識が無いとパフォーマンス考慮やエッジケースの想定、可読性と保守性を優先したコードの選出や適切な指示が出来ず、ただ動くだけでなにも想定されていない負債だらけのコードを量産します。
-
この事を今回のタイピングゲームとLP作成で身をもって痛感したため、改めて基礎から立ち返り、またAIの使い方も変えて日々学習しております。
また余談ですがCS(コンピューターサイエンス)の知識が重要だと思ったため、書籍等を活用し、内部の構造から説明できるよう励んでいきたいと思います。
終わりに
今回はAI時代だからこそ基礎力や知識を身に付けていくことが大事というテーマで書きました。
AIでエンジニア不要!みたいな投稿を見かけますが、鵜吞みにせず自分のペースで知識を蓄え続けるのが大事です。情報の取捨選択は結局自分なのでね!有益だったりこれは使えそうってのだったり、知的好奇心のままに動くのがいいかもですね!
ここまで読んでいただきありがとうございました!