デバッガを使ってはいけないデバッグの話


CAREER SKILLS ソフトウェア開発者の完全キャリアガイド 第32章「デバッグ」で語られていた内容が自分にとって新発見だったので、それをまとめてみます。

本章のキーフレーズ

  • ソフトウェア開発者である限り、コードのデバッグにはかなり多くの時間を費やす必要がある
    • であれば、デバッグは得意分野にしておくべきではないか?
  • デバッグはマインドセットの問題
  • デバッガを使うのは...
    • 最後の手段であるべき
    • 明確な目的を持って、何を確認すべきかが明らかである場合にのみ使用する
    • 周囲を見渡すため にデバッガに入ってはいけない
  • 問題仮説を立てたら、そのチェックにユニットテストを活用しよう

デバッグとはなにか

コードベースに含まれる問題の源を見つけるためのあらゆる作業のこと。

考えられる原因を特定し、究極の根本原因が見つかるまで仮説を検証し、最終的にその根本原因を取り除き、二度と同じことが起きないようにすることである。

引用: CAREER SKILLS ソフトウェア開発者の完全キャリアガイド 第32章「デバッグ」

デバッグと聞けば、print文を挟んでみたり、デバッガ片手に動作を確認したりする作業をイメージしがちですが、実際はもっと広い意味で問題解決を行うことである、と本書は述べています。

デバッグの原則: デバッガを使うな

なかなかセンセーショナルな書き出しです。中身を読み進めると納得できました。

予め述べておくと、筆者の主張はデバッガの有用性を否定するものではありません。むしろ、デバッガ自体は非常に強力で素晴らしいツールであると筆者自身前置きしています。しかしながら、問題解決のプロセスにおいてデバッガは出発点ではないのだとも述べており、闇雲に安易にデバッガに手を出すことを否定しています。

どうやってデバッグを進めていけばよいのか、以降で解説します

エラーの再現

再現できない問題はデバッグできません。

再現性を確保できなければ、フィックスの確証を得ることもできません。よって、ゴール(問題の根本解決)に到達するために最初にすべきことは、問題を再現することです。

問題が「間欠泉」的なもので確実に再現できない場合は、まだ問題を再現するための条件がわかっていない、ということです。自力で再現ができない場合は報告者の協力を仰ぐなりして助けを求めましょう。

Column: とはいえ、ねぇ...?

所謂「間欠泉」的な問題について。

上司からFixしろと言われてるんだけど。。。

という質問があったようで、筆者がコラムで回答しています(辛いけど、まぁよくあることですね...)。

とはいえ、再現できない問題はまだデバッグすることはできません。だから、仮に自力で再現方法を見つけられなくても、より多くの証拠を集めることはできるわけで、まずはそこから始めてみましょう。

print文を追加して情報を集めるであったり、人工的に問題が発生した環境を再現できる方法を考えてみるなりしましょう。

情報を集めないまま当てずっぽうに進めて問題を解決できる確率は低いし、直ったかどうかの確認すらままならないことになります。

落ち着いて考える

今現在バグと向き合っていない、冷静な読者のみなさま(そして私)からすれば、これは一見当たり前の主張のようにも思えます。
が、本書はここにわざわざセクションを割いて解説しています。強い意図を感じさせます。それだけ、デバッグの現場では(解決を焦るあまり)忘れがちであり、かつ省いてはならない重要なステップである...というメッセージなのでしょう。

システムの仕組みを考え、問題の原因になりえそうな場所に仮説を立てましょう。ソースコードを読み、本来システムがどのように動作しなければならないか、情報を集めましょう。

次のステップに進むためには、検証できるアイデアを持っている必要があります。

なお、まだデバッガの出番ではないです。

仮説の検証

はい。まだデバッガの出番ではないです。

ここで使うのはユニットテストです。仮説を検証するためのユニットテストを書くのです。

先のステップで立てた仮説に基づき、仮説した問題を暴けるようなユニットテストを書きます。
こうすることで得られる効果がいくつかあります。

仮説があたっていた場合は、そこから問題解決を導くことができます。また、再発防止するためのユニットテストが同時に手に入ります。ユニットテストを追加したことで、システムの堅牢性を少し向上させることができます。

仮説が外れていた場合でも、可能性を1つ、確実に狭めることができます(もちろん、ちゃんと意味のあるテストを書いていることが前提ですが...)。信頼できる足がかりを作ることはデバッグの旅路を確実に前に進めてくれます。

これは結構馬鹿にならない話で、非常に含蓄ある一節がありますので本書の内容をそのまま引用します。

デバッガ頼りが悪い理由のひとつは、何を探していたのか忘れたり、しっかり調べたことを信頼していなかったりで、仮説をチェック、再チェックする過程で同じ間違った道を何度も何度も通りたくなってしまうことだ。

引用: CAREER SKILLS ソフトウェア開発者の完全キャリアガイド 第32章「デバッグ」

ユニットテストは 登山で大きく後退するのを避けるためにアンカーを打つのと似ている と、本書は述べています。上の引用文と併せて読むと、非常に的確なメタファーだと感動すら覚えます。そして、なんだか打ちのめされた気持ちになります(私自身、見に覚えのありまくる話で刺さりました)。

しかしながら、現場によっては仮説検証のユニットテストを書くことが極めて難しい状況というのも存在します。そういった場合であれば、「デバッガを使う目的」を明確にした前提でのみ、デバッガを使うことが認められます。

思い込みをチェックする

「このコードは当然こうなっている/こう動くはず」という思い込みを、ユニットテストによってチェックします。

仮説や、そのために拵えたユニットテストは、そのほとんどが「自明に見えること」を改めて確認するだけに終わります。大半の仮説は外れるでしょう。しかしながら、その中には予想を裏切る結果を返すものもあります。

全てが自明であればそもそも問題自体発生していないのだということを忘れないようにしましょう。「XXXするのは当たり前だから改めてチェックする必要はないだろう」という気持ちを持たないように。

分割統治

概念はソートアルゴリズムなどでも出てくるあれと同じです。

システム(あるいは特定機能に対する入力など)をそのまま扱っていたのでは、問題の核心を捉えることは難しいです。

領域を絞りながら、それでも問題を再現できるようにする方法を考えて検証していくと、問題特定が比較的に容易になります。

採るべき方法は場合によって変わってきますが、システムの無関係そうなコンポーネントを切り離してみたり、入力値を分割してエラーの発生可能性を狭めてみたり、などのアプローチがこれに当たります。

フィックスしたあとで理由を理解しよう

修正したコードは意図せず他の問題を引き起こすかもしれません。もっと悪いケースだと、根本の問題を実は解決していないかもしれません。

  • 最初に起きた問題はなにか
  • 自分の対処方法は、その問題をいかにして解決したのか

これらを理解しましょう。これらがわかっていない状態は「問題解決」といえず、「問題を隠した」だけになってしまいます。

筆者によれば、小手先の修正でコードが正しく動作するように見えたら、(理由がわからずとも)問題はフィックスしたと見なす 開発者があまりにも多いのだそうで(私も心が痛い)。しかし、これは危険な癖であると筆者は警鐘を鳴らします。

まず、適当にコードをいじり回してしまうことで新たな問題を作り出してしまう危険性があること。具体的なリスクの話でこれはわかりやすい。しかし、それ以上に深刻なのは、その習慣に自分が慣れてしまうことです。それはテクニックと呼べるものではないし、スキルセット足りえるものではありません。

また、フィックスした理由を確認するだけでなく、フィックスしたこと自体も確認することが必要です。小手先の修正で見かけ上システムが動作するようにコードを修正したとして、それがQA部門の手に渡ったとき(あるいは本番デプロイ後)に問題が再発してしまっては多くの時間が無駄になります。可能な限り、その問題を検証する回帰テストを書くようにしましょう。問題をきちんと理解できていれば、適切なユニットテストが書けるはずです。

最後に、同種のバグが他にないかを探します。同じ問題によって、別の場所で困った症状が引き起こされている可能性は高いです。フィックスした理由を理解できていれば、今回の問題が他の場所でどう派生しうるかのアタリもつけられることでしょう。

さいごに

安易にデバッガに手を出してしまう癖は自分もあったので大いに反省するとともに、ユニットテストの効能を発見する新たな学びになりました。末節にいきなり飛び込むのでなく、まずは問題を理解し、仮説を立てながら系統的に問題にぶつかっていく姿勢を持つこと。正しい姿勢を身に着けて経験を積み、スキルに変えていきましょう、ということですね。非常に参考になりました。