開発のほとんどはデバッグですが、デバッグのやり方をしっかりと教わることはあんまりないと思うので、開発初心者向けにデバッグの考え方をまとめてみました。
TL;DR
- ベテラン開発者にとっても、開発のほとんどはデバッグ。デバッグの効率を上げることで、開発効率が上がります。
- 「無闇に検索」や「漠然とコードを眺める」ではバグは解消されません。細かく実行し、観察しましょう。
- バグの原因は自分の勘違いかもしれません。自分の理解も客観的に観察すると、効率よくバグが解消できます。
初めに:開発のほとんどはデバッグ
初心者で開発をしていると、「ずっとデバッグしてるな」と落ち込むかもしれません。先輩エンジニアが綺麗な動くコードを量産してるのに、自分が書いたコードはエラーを吐き続け、全然思い通りに動かなかったり。
先輩エンジニアも最初から完璧なコードを書いている訳ではありません。初心者プログラマーと同じように、バグだらけのコードを書いてから、それをデバッグすることで動くコードを作っています。動くようになったコードだけをコミットしているので、完璧なコードを苦もなく作っているように見えるのです。感覚ですが、開発時間の8〜9割はデバッグに使っていると思います。
逆にいうと、強いプログラマーとは「バグのないコード素早くを書ける人」ではなく「バグを素早く解消できる人」なのです。
この記事では、デバッグの原因を整理して、効率よくデバッグする方法を整理します。
バグにも色々ある
バグは「思い通りにプログラムが動かないこと」ですが、一言で「バグ」と言っても実はたくさんの種類があります。エラーで実行が止まることもあれば、エラーが起きずに期待した値が返ってこなかったり、実行されるはずの関数が実行されていなかったり、メモリーエラーなど条件によって発生するバグだったり。まずバグの分類をして、効率的なデバッグの方法を考えていきます。
バグの分類
デバッグのやり方に強く影響を与える「書き方 vs ロジック」という軸と「書きたかったコード vs 書かれているコード」の軸でバグを分類して見ます。
凡ミス
「書き方について、書きたかったコードはあっているが、書かれているコードが間違っている」パターンです。「変数名の打ち間違え」などの凡ミスですね。実際のコードでは、このタイプのバグが一番多いです。
このタイプのバグは、どんなに検索しても見つかりません。コードを淡々と眺めていても、気づけないことが多いですね。
この場合は、エラー文をしっかりみてあげることが大事です。「undefined variable」などが書かれていればこのパターンかもしれません。
書き方の勘違い
「書き方について、そもそも書きたかったコードが間違っている」パターンです。「変数の順番を勘違いしていた」「javascriptのfor ... of ...の挙動を理解していなかった」などの、勘違い・知識不足が原因のバグです。
この場合、そもそも勘違いしているので、どんなにコードを眺めても気づけません。エラー文を見て気づけることもありますが、非常に稀ですね。このタイプのバグは、検索したり、本を読んだりして情報のインプットをしないと解消しないでしょう。
ですが、新しく書き方を勉強するチャンスでもあるので、ワクワクするバグではあります。
ロジックの実装ミス
ここからは「書き方」ではなくて、ロジックについてのバグです。
最初に「書きたかったロジックが実装されてない」タイプのバグです。
ロジックについてのバグは、そもそもエラーが出ないことが多いです。なので、「エラーを見て解消する」ことはできないです。また、検索をしても解消はしないですね。
ロジックの設計ミス
最後に「そもそもロジックを勘違いしていた」というタイプのバグです。
これもロジックについてのバグなので、エラーが出ないことがほとんどです。また、よくあるロジックを実装しているとき以外は、検索してもヒットしないことが多いでしょう。
なので、ロジックを一つ一つ確認する以上のデバッグ方法はまずないです。
まとめ
デバッグでは「自分は正しく理解しているか?」と「自分の理解は正しくコードに反映されているか?」の両方を「書き方」「ロジック」の両側面で見る必要があります。さらにバグに直面しているときは、どのタイプのバグなのか?はわからないので、全ての可能性を想定しながらデバッグを進める必要があります。
こう考えると、「コードを眺める」や「とりあえず検索する」というのは非効率であることがわかります。
コードを眺めても「自分の理解は正しくコードに反映されているか?」しか確認できません。「勘違い」が起因のバグは絶対に見つけられないのです。
検索しても「自分は正しく理解しているか?」の確認しかできません。凡ミスによるバグは見つけられないのです。
効率的なデバッグ方法
これまで、「バグの分類」と「非効率なデバッグ方法」についてみてきました。これを踏まえて、どのようにデバッグをすると効率が良いか?を考えていきます。
コードの挙動を観察しよう
バグとは「自分の想定と実際の挙動が異なっている」ということなので、自分の想定と実際の挙動を比較するのが一番の近道です。
なので、「コードを読む」代わりに「コードを実行する」ようにしましょう。コードが正しく動いているか?は読んでいてもわかりません。実行して確認するものです。
コードの挙動を観察する最初のステップが「エラー文を読む」ことです。エラーとは、「書かれたコード」をうまく実行できない時に、コンピューターから発せられるメッセージなので、バグについての重要な情報を含んでいる可能性が高いです。
実際、バグの8割くらいは、エラーを正しく理解すれば解消します。バグのほとんどは簡単な凡ミスだし、よくあるミスに対してはわかりやすいエラーが投げられることが多いです。なので、まずエラーをしっかり読む習慣をつけましょう。
「エラーは英語だしよくわからない」と感じることも多いですが、理解できないエラーに出会った時は検索しましょう。ここで重要なのは「解決策を検索する」のではなくて「エラーの意味を検索する」ことです。
エラーを読んでみても解決しなかったら、次は「print文を挿入してコードを実行」してみましょう。「print debug」と呼ばれるデバッグ方法です。エラーが発生している部分の周辺で、各変数がどんな値になっているか?という「コードの挙動」を観察することで、「自分の期待通りに動いているか?」が確認できます。例えば「この変数の値が変なので処理できません」というエラーであれば、その変数の値を表示してみて「本当に変な値になっているか?」を確認したり、「その値を計算する関数への入力はどうなっているか?」を表示したりします。
print debugは効率的なツールややり方がたくさんあるので、調べてみると良いかもしれません。
重要なことは、「コードを読むくらいなら、実行しよう」という考え方です。コードを実行することで「自分の勘違い」の囚われずに「実際の挙動」に向き合うことができるので、幅広いタイプのバグに対して対処ができるのです。
実際、デバッグでコードを読む時間は10%くらいじゃないかと思います。「どんな条件で実行したらバグの原因がわかるか?」を考えて実装している時間が長いです。
コードの想定挙動を言語化しよう
エラー文を読んだりprint debugをしたりと、コードの挙動を観察することに慣れてきたら、「コードの想定の挙動を考える」ことをお勧めします。想定とは「この時点でこの変数はこの値なはず」であったり「このタイミングでこの関数が実行されるはず」などです。
想定を言語化すると、コードの挙動を効率よく観察できるようになります。print debugをするときに、「想定の値」と「実際の値」を同時にprintすればいつ想定とずれたか、一目瞭然です。
実行されるはずの関数にprint文を仕込んでおけば、正しく実行されているか?が観察できます。
さらに、複雑なエラーの場合、assert文(存在しない言語も、、、)を使ってデバッグするのもお勧めです。コードの中に、「この時点でこの変数はこの値になっているはず」などの「成立するはずの条件」をプログラムの中に書き込んでしまい、その条件が成立していなかったらエラーを発するようにします。こうすることで、「想定と現実がずれ始めた瞬間」がわかります。
想定挙動を言語化することは、「ロジックのバグ」に強い力を発揮します。ロジックのバグはエラーが発生しないことが多くて、printしてみても「あってるのか間違ってるのかわからない」ことが多いです。この場合は、丁寧に自分の想定を言語化していきましょう。想定を言語化するだけで、凡ミスに気がついてパッと解消することも多いです。
最後に:完璧にできなくても問題ない
デバッグでは心が削られていきます。自分が一生懸命書いたコードが思い通りに動かず、原因もわからない状況下に長く置かれるのです。さらにリモート勤務で、先輩プログラマーがデバッグしているところを見る機会もなく、あたかも一度で完璧なコードを作っているかのように感じてしまうと、自分の無力感に苛まれることも多いです。
そんな中で、「最高に効率的なデバッグ」なんてできるはずがありません。経験豊富はプログラマーであっても、「なんで動かないのーーー」と叫びながら、ただコードを眺めて時間が過ぎたり、、、なんてことが頻繁にあります。
最初から全てを完璧にこなす必要はありません。どうもうまく解消できないバグに直面した時に「そういえばprint debugって言ってたな」とか「自分の想定を言語化してみるか」と思ってもらえれば良いと思っています