🔎

デバッグの思考法(問題の定義と分析について)

2023/04/13に公開

つまり

デバッグは楽しいです。

基本的な考え方を抑えておくことで、
目の前のバグがどんなに複雑でつかみどころがないように見えても
解決に向けて動き出すことができます。

この記事の内容

言語やフレームワークに関係なく、ソフトウェア開発において抑えておくべき(と私が考える)
「デバッグの思考法」をご紹介します。

エンジニアにより執筆されエンジニアによって読まれる記事には
デバッグツールの使い方についての解説はあるものの
デバッグを進めるときに何を考えて取り組めば良いのか?
という根本の考え方に踏み込んだ話はあまり見当たりませんでした。

特にソフトウェア開発においてあまり経験が深くない方にとっては
この考え方を知っている(あるいは無自覚に理解している)のと知っていないのとでは「見える世界が全く違う」と思っています。
「自分より優秀な先輩が、自分には思いつかないような箇所に目を付けて
限られた情報からすぐに答えを出してしまう」という芸当に感銘を受けたことがある方もいるのではないでしょうか。

ちなみに、この現象について解説している面白い記事がZennにあったので、
先達への敬意を込めて紹介させていただきます。
https://zenn.dev/suzuki_hoge/articles/2020-11-logical-debugging-e46a81aa4eb61e0caa5e

そして本稿では、上のように思いもよらぬ早さでデバッグを進める方の頭の使い方
他の人にも理解・実践できるようにすることを目指しています。

まるで私が優秀なエンジニアの思考を完全に体得しているかのような書き振りで恐縮ですが、
正直に申し上げて私自身「思いもよらぬ早さでデバッグができる」とは全く思っておりません。

あくまでも、私がこれまでに
「周囲のエンジニアの優秀さに圧倒され、近づこうと試行錯誤し」
「同じような壁に直面しているであろうエンジニア初学者の成長に向き合ってきた」
経験に基づき、現時点での仮の答えとして記させていただきます。
読者の皆様からはぜひ、忌憚の無いご意見を頂きたいと思っております。

前置きが長くなりましたが
以下、本題です。

概論

デバッグは「問題の定義」「問題の分析」に分けて考えることができます。

問題の定義とは、「期待値」と「実際の挙動」のギャップを捉え、解決すべき問題を正しく理解する行為を指します。デバッグが効率的に進まない場合、案外ここの「問題の定義」がうまくできていないことが多いです。デバッグとは問題を解決するための手段として認識されている方もおられるかと思いますが、本稿ではあえて「解決すべき問題を正確に定義すること」にも焦点を当て、デバッグに欠かせない要素として位置付けたいと思います。

問題の分析とは、上記で捉えた問題(期待値と実際の挙動のギャップ)の根本的な原因を突き止めていく行為を指します。問題を定義した時点で何が起きているのか大まかに認識はできるものの、「具体的にどこでどんなバグが生じているのか」「どこに修正を加えてどう改善するべきか」を具体的に把握できていないと思います。ギャップとして認識された問題の原因をより具体的に理解し、解決に繋げるには、分析の仕方に工夫が必要です。

問題の定義

問題の定義とは、「期待値」と「実際の挙動」のギャップを捉え、解決すべき問題を正しく理解する行為を指します。

問題とは、期待値と実際の挙動のギャップである

期待値は、ここでは「本来のあるべき動作・挙動」とします。自分の開発しているソフトウェアの機能において、正しく実装されている場合にはどんな挙動をすべきなのか? を定義したものが期待値です。おそらくソフトウェアテストの文脈では期待値という用語が使われたりもするので、馴染みがある方もいるかと思います。

実際の挙動についてはあえて説明するまでも無いかもしれませんが、言うなれば「実際に観測された動作・挙動」のことです。開発した結果としてソフトウェアがどんな挙動をしているのか?その状態を指します。

上記で挙げた「期待値」と「実際の挙動」にはギャップが生じることがあります。本稿ではこれを「問題」として位置付けます。例えば、「本当は画面に〇〇と表示されて欲しい(期待値)けど、××と表示されている(実際の挙動)」みたいな状況は期待値と実際の挙動にギャップが生じている=問題がある状況といえます。

問題の定義は最初のステップとして欠かせない

分かりきったようなことしか言っていないように見えますが、意外と重要な話をしています。

「なぜ問題を定義する必要があるのか」を理解してもらえるよう、例を交えてご説明します。

あなたは、あるECアプリの開発を担当しているとします。
画面がおかしな挙動をしていることに気づきました。
【税抜100円】の商品が、【税込108円】で表示されているのです。

2023/04/12時点、日本の消費税率は10%です。
これにもとづくと税込価格は【110円】であるべきです。

これはいけない、とあなたは大急ぎでバグ(?)の修正に取り掛かります。
hotfixブランチを作成し、価格計算ロジックを読み、【TAX_RATE】が【0.8】になっている箇所を見つけ
この値をすぐさま【1】に変更してコミットし、本番環境に反映しました。

達成感を味わいながらひと段落ついていると、ユーザーからクレームが届きました。
「税率8%で計算するはずの画面で、税率10%で計算されている」だそうです。

あなたは何か嫌な予感がして、先ほど修正した画面をもう一度よく見直しました。
なんとこの画面は、「2018年時点の消費税率(8%)に基づき参考価格を表示する」ためのものだったのです
(そのような機能が実在するかは不明です)。

つまりあなたが「バグだ」「問題だ」と認識していたものが、実は問題ではなかったのです。

「問題の定義」を怠り、期待値と実際の挙動の両方を正しく把握していない中で問題に見えるものを解決しようとすると、このようなことが起こりえます。

逆にいえば、期待値と実際の挙動の両方を正しく把握し、問題を定義するための習慣があれば
問題が何か(そもそも問題があるのか)を正確に把握できる
ようになります。
デバッグにおいても、問題を正しく定義することで初めて、デバッグによって何を解決すべきなのかがわかるようになります。

問題を正しく定義する

問題を正しく定義するためには、「期待値」と「実際の挙動」の正しい認識が必要です。

実際の挙動についてはそこまで難しい話ではないので、強いて言えば「事実」や「データ」に目を向けましょう、といったところです。例えば、「レスポンスが返ってくるのが遅い」は個人の解釈ですが、
思い込みを排除した上で「レスポンスには〇〇msかかっており、SLAとは××msの差がある」のような事実を捉えるようにしましょう。

意外と起きがちなのが、期待値の認識への甘さです。上述の例でも、本来の正しい動作を把握していないために見切り発車で解決に乗り出そうとしていました。ソフトウェア開発においては、おそらく要件が決まっていることがあると思います。問題らしき動作を見かけた際(あるいは他の人から「問題があります」と言われた場合)には、「じゃあ本来どう動いているべきなのか?」に目をむけ、ドキュメントを読んだり関係者に相談したりする、という習慣を持つと良いでしょう。

(おそらく私の職場で私に何か相談しようと来られた方は、私が最初に「これ結局どう動いてればいいんでしたっけ」「ドキュメントではどう定義されてます?」みたいなことを聞いてくるのを体験していることと思います。)

こうして正しく認識した「期待値」と「実際の挙動」を見比べた時に違いがあれば、それこそが問題であり、デバッグに向けて本格的に動き出すことができます。

余談

「問題とは、あるべき姿と現状のギャップである」という文言は、
ことビジネス書においては多くみられます。

私が観測し、すぐ引き出せた範囲では

問題が存在すると判断するとき、通常あなたは、今までの延長線上の結果と本来望んでいた結果の間にギャップを感じつつあります。
考える技術・書く技術―問題解決力を伸ばすピラミッド原則 (p.171)

つまり、問題とは一言で言うと、「目標(あるべき姿)と現状とのギャップ」ということになる
問題発見プロフェッショナル―「構想力と分析力」(p.16)

「問題」とは目標値や現状との間にあるギャップを指し、「課題」とはギャップが生まれている理由・背景、そしてそのギャップを埋めるために乗り越えなければならない障害・ハードルのことを指す。
「専門家」以外の人のためのリサーチ&データ活用の教科書: 問題解決マーケティングの秘訣は、これだ! (p.110)

「問題」とは「ギャップ」である。何と何の?「現状」と「目標」の、だ。目指す先と現在地の間にある差分、それが「問題」だ。問題とはつねに、今の状態と理想の状態を〝比較〟したときに表れる。
戦略コンサルタントが大事にしている目的ドリブンの思考法 (p.195)

そもそも「課題」とは何か?「課題」とは、「<あるべき姿>と現状のギャップ」だと定義される。
問題解決――あらゆる課題を突破する ビジネスパーソン必須の仕事術(p.208)

従って、業界のKFSがきちんと理解できていれば、個別事業戦略を具体的に策定する際、自社の能力と業界のKFSとのギャップを考慮して自社の合理的競合ポジションと現実的な目標水準を決定でき、自社に固有の最適戦略案を案出することが可能になるのである。
戦略策定概論―企業戦略立案の理論と実際(p.104)

他にもあった気がします。

特に引用元を示すことがなく上述のように文言が使われることが多いので、
私自身この発想が誰によって生み出されたものなのか正確に把握しておりません。
ご存じの方がいればご教授ください。
(「問題発見プロフェッショナル」によると1979年にハーバート・A・サイモンが自著で述べているらしいから、少なくともその時期にはある考え方?)

問題の分析

問題の分析とは、上記で捉えた問題(期待値と実際の挙動のギャップ)の根本的な原因を突き止めていく行為を指します。

前章で問題を定義した段階で、どんな問題が生じているのか(ギャップが何か)の把握はできていても、具体的にどこにどんな原因が潜んでいるのかまでは把握できていません。問題の分析によって、問題を引き起こしている原因を解決可能なレベルまで具体的に突き止めて初めて、問題は解消されます。

また、問題を細かくみていくなかで、闇雲にコードを見始めてしまったり、手当たり次第情報を見ていこうとすることは非効率です。より効率的に問題の具体化をするために、「構成要素と全体像を抑え」「仮説を立てつつ」「重要なものから深掘りする」ことが必要です。

ちなみにこの章は前章と比べて私見が多いです。可能な限り参考文献を追記していこうと思っています。

構成要素と全体像を抑える

構成要素と全体像を抑えることで、分析の見通しを立てることができます。
もう少しわかりやすい説明をすると、「ある目的を達成するためには、どんな構成要素が必要で、それらが全体としてどんな構造をとっているのか?」を理解する ことで、より細かい分析に進むための手がかりが得られます。

まだピンとこないかもしれないので、また例を示します。

Ruby on Railsを利用して、TODOを追加する機能を作るとします。
この時、期待値は「TODOが追加できる」というものです。

その実現には何が必要でしょうか?
ざっと洗い出してみると

・モデル
・ビュー
・コントローラ

といったところでしょうか。

今洗い出した中で、「モデル」や「ビュー」が【構成要素】です。
そして、各要素が全体として取っている構造を考えてみると
「ビューで値を受け取りコントローラに渡し、モデルを作成して登録する」
という【全体像】が見えてきます。

上述の例では「TODOを追加する機能」の実現を目的とした上で
構成要素と全体像を洗い出しました。

これがわかると、期待値と実際の挙動をより具体的に比較するための判断材料が得られます。

つまりTODOを追加する機能に問題が生じているのであれば
期待値と実際の挙動の両方における

  • モデル
  • ビュー
  • コントローラ

を比較すれば違いがより具体的に見えてくる、ということがわかります。

仮説を立てて検証する

何が根本の問題なのか?について正確な答えが出る前に 「今の時点での自分なりの考えや見通し(=仮説)」 を立ててみましょう。

全てを見終わる前に答えがわかるわけない、と思われるかもしれませんが、
意外とできますし、仮説の有無は効率に影響してきます。

仮説というのは仮の答えであり、
前項のRailsの例を用いて考えれば「モデルに問題がありそう」という見立てを考えてみることが、
仮説を立てることです。

なお、あくまでも仮説というのは正確な答えではないので
それが本当の答えなのかどうかを検証してみる必要があります。

上では「モデルに問題がある」という仮説を立てたので、
それ以外の要素が正しく動いていることを検証してみましょう。

まずビューの挙動を確認してみる。
コントローラにブレークポイントを設置すると、ビューから渡したデータを正しく受け取れていることがわかった。

次にコントローラの挙動を確認してみる。
ブレークポイントから一行ずつ処理を進めてみると、インスタンスの生成や加工といったステップも問題なく進んでいく。

最後にモデルのインスタンスに「save」メソッドを呼ぶ処理を実行してみる。
するとエラーが発生した。

上のように、
各構成要素の挙動を確認しながら「どの要素に問題があるのか」の検証をしていきます。
これにより仮説である「モデルに問題がありそう」という見立てが正しかったことがわかりました。

補論:仮説をどうやって立てるか

既知の事実に基づいて仮説を立てることが可能です。
例えば「同じビュー・コントローラでも、TODO以外のモデルでは正しく登録ができていた」という観測事実があれば、概ね「ビューとコントローラではなく、モデルに原因がありそう」という発想が出てくるかと思います。

問題箇所をさらに深掘りする

ここまでは、「全体を構成する要素のうちどのあたりに問題があるか」を突き止めることができた段階です。
この時点でまだ解決に繋げられるほど具体化できていない場合は、その「あたりをつけた問題箇所」に対して前項までの分析ステップを繰り返すことができます。

例えば先ほどの例で「モデルに問題がある」ということがわかっているのであれば
そのモデルの期待値と実際の実装を比較していきます。

モデル全体を構成要素に分け、

* クラスの定義
* ApplicationRecordの継承
* 各プロパティのバリデーションの設定
* スコープの定義

が必要であるとして整理します。

その中でもどこに問題がありそうなのか当たりをつけてみて、本当にそこが問題なのか検証します。

そして判明した問題箇所が、本来あるべき実装内容と比べてどうなのかを比較してみると、
バグの原因と言える場所がほぼ確実に特定できるのではないでしょうか。

まとめ

デバッグの思考法を、「問題の定義」「問題の分析」として整理しました。
問題を正しく定義し、分析を効率的に行なっていくことで
より素早く正確なデバッグを実行できるでしょう。

デバッグは楽しいです。

一見つかみどころがなさそうな目の前の事象に対して切り口を見出し
仮説を検証しながら具体的な理解を進めていく行為には、知的な面白さを感じられます。

本稿を通して、
皆さんが目の前のバグに立ち向かう指針を手にし
知的欲求を満たせるデバッグを楽しんでもらえれば幸いです。

Discussion