「AWS無料相談会」をオンラインで開催中

新人プログラマの時に知っておきたかった「ハマったときの考え方とフローチャート」

はじめに

プログラムを書いたことがある人なら、誰しも「ハマる」という状況に陥ったことがあると思います。

ハマるとは、一般的には何かから抜け出せなくなってしまうことを意味しますが、システム開発の世界では、ある課題やエラーなどに対して、解決の見込みが見えないまま多くの時間をかけてしまうことを意味します。

今回は、ハマってしまったときに、いかに問題を解決し、ハマった状態から脱却するかについて書きたいと思います。

目次

問題解決における6つの基本

最初に、問題解決の基本的な考え方を書いておきます。当たり前のことばかりだと感じるかもしれませんが、この基本が完璧にできていれば、ハマることはそもそも稀だと思っています。

もし、普段の実装の中で「同じことで丸1日悩んでいるけど解決の手立てがない」「これにさえ気付いていればもっとはやくできた」「単純なミスだった」といったことに時間を溶かしているのであれば、問題解決の基本ができていない可能性があります。

技術的な知識が欠けていることがハマる原因になっているケースもあります。しかし、理想通りにいかないことも多いとはいえ、適切な問題解決を行うことでハマる時間を減らせるかもしれません。

以下では、自分自身の経験や信頼できる書籍から学んだ問題解決の基本を、6つに分けて紹介しようと思います。

1. イライラしない

1番重要かもしれませんが、イライラしないことが大事です。

自分も最初の頃よくやっていた(いまも油断すると…)のですが、「あーあれもこれもだめだ、全然わかんない!イライラする!」みたいなことが初心者だと起こりがちだと思います。気持ちはとてもよくわかるのですが、これは残念ながら、問題解決につながらず、冷静になれないので効率は悪くなり、せっかくのコーディングが楽しくもなく、何のメリットもありません。

こういう時は、考えずに、ただ問題を解くことに集中するべきです。そのために、淡々と問題を解くための自分のルーティンのようなものをもっておくことが大事だと考えます。冷静に考えてみれば、実装の失敗は「この方法はうまくいかなかい」という1つのデータでしかなく、イライラする必要はないと気付くはずです。

また、ドツボにハマる前に休憩や気分転換をするというのも有効かと思います。バグなどの謎の現象に立ち向かうも闇が濃く、どうしても沼から脱出できない時に見るフローチャートという記事では、気分転換のタイミングをわかりやすく図で示していておすすめです。

2. 常に解決の計画を持つ

常に解決の計画を持ちましょう。これは、何か調査や検証をするときに、明確な目的をもって取り組むということです。

自分が昔あったのが、「なんとなくあれが原因な気がするから、とりあえずその辺を調べてみよう」といった発想です。過去に類似している問題があり、自分の中である程度確信があればいいのですが、特に根拠もなく調べて時間を溶かしてしまうことが起こりがちだと思います。

闇雲に調べたり、コードを書いたりせず、常に計画を持つことが大事です。これは、達人プログラマーという書籍の慎重なプログラミングの方法にも詳しくかいてあるので、一部抜粋します。

  • 偶発的なプログラミングを避け、慎重なプログラミングをする方法
    • 目隠しでコーディングしてはいけません。完全に理解していないアプリケーションを作成しようとしたり、なじみのない技法を使おうとするのは、偶発的なプログラミングに通じる近道なのです。
    • 明確なプランがあなたの頭の中にあるか、ナプキンの裏に書かれているか、CASEツールから印刷したタペストリのようなものであるかどうかは別にして、まずプランから進めるようにしてください。
    • 信頼のおけるものだけを前提としてください。偶然や仮定に依存してはいけません。特定の状況下にあってそういった区別が行えない場合は、最悪の仮定を置いてください。
    • 仮定をドキュメント化してください。他のメンバーとのコミュニケーションを効率化したり、あなたの心の中にある仮定を明確にするには、「契約による設計」(p.123)を参考にしてください。
    • 単にコードをテストするのではなく、あなたの仮定をテストしてください。推量は抜きにして、実際に試してみるのです。あなたの仮定を試すため、表明(「表明プログラミング」(p.139)参照)を記述してください。その表明が正しいものであれば、それだけでコード中のドキュメントの質が向上したことになります。そして仮定の誤りが発覚したのであれば、単にあなたは運がよかっただけだということを理解できるはずです。

一貫して主張されているのは、確かな仮定のない実装は良くないという点です。計画を持ってから実装に入ることの重要さがわかると思います。

3. 問題を分割する

問題を切り分けることも重要です。これは常に解決の計画を持つにも関連しますが、問題を分割できないと、計画を持つことも難しいと考えます。具体的には以下のような方法があります。

  • 簡単な問題に切り分ける
  • 部品毎に問題を区切る
  • 解けると考えられる大きさまで問題を切り分ける

同じようなことを言っていますが、特に重要なのは解けると考えられる大きさまで問題を切り分けることです。初心者がやりがちなのが、一気に実装をして、2つ3つの要素が混ざり合い、何がエラーの原因なのかわからなくなってしまうことです。参考までに、ソースコードって実際のところどういうふうに書いていますか?という記事では、以下のように書いてあります。

それから実際にコーディングにとりかかるわけですが、ここで非常に気をつけているのは、できる限りコードを正常動作する状態に保つことです。大きな変更を一気にやろうとするとわけがわからなくなるので、できるかぎり小さな変更を積み重ねていくことで最終的に構想通りの変更を行うようにしています。コードを書いている途中、コンパイルしてユニットテストが通せるまでは、危ない橋の上を歩いているような気分で、なるべく早く安定した状態に戻したいという気持ちがあります。

このレベルで慎重になれば、エラーの原因は特定したも同然で、実装しながらにして問題の分割ができることとなります。また、ペアプログラミングして気がついた新人プログラマの成長を阻害する悪習という記事では以下のように述べています。

すぐさま、私は「シンタクスチェックの仕方」と「画面上でのモジュールの実行」の仕方を教えた。また、それを利用した「小さなテストコードの書き方」も教えた。ところが、彼はその方法は知っていたのだ。しかし、むしろめんどくさいことのように感じていた。なぜかと問うと応えには窮していたが、要するに「最終的に画面が表示されるのだから画面を見た方が完成に近いだろう」と言うことだった。

CIは導入され、テストコードを書くことが必須となっていったあとになると彼はブラウザ上で動作するのを確認したあとに、めんどくさそうにテストを書いていた。

確認のサイクルが長くなり、結合されたあとであるので問題は複雑化した形でしか、エラーメッセージは出力されていなかった。あまり読んでいなかったので気にならなかったかもしれないが。

単体でのテストを意識せずに書くからか、コードの結合度は高く、テストの難易度も上がっていった。

テストの話はこの記事では置いておくとしても、コードの結合度小さな単位でのコードの書き方は、問題の分割において重要な考え方だと思います。コードが複雑化してしまうと、問題を切り分けることが難しくなってしまうからです。

4. ツールを使う

デバッグのためのツールを使いましょう。ツールは以下の項目を満たせているものが良いと思います。

  • エディタ上でシンタックスエラー、型エラーなどを検知できる(コンパイルを待つ前にエラーを検知できて高速)
  • ライブリロードができる
  • StackTraceが確認できる
  • BreakPointを設置できる

ただし、これらに頼るのではなく、自分の頭で道筋を立ててから使うといいと思います。プログラマーの開発速度は「はまる」時間の長さで決まるという記事では、以下のようなことをいっています。

問題箇所を頭で考えることに慣れてくれば、大抵の場合、テキストエディタさえあれば問題は割とすぐ解決できる。

そして、ツールが本領を発揮するのは、当たりを付けていないプログラマーに対してではなく、当たりを付けているプログラマーに対してである、ということを忘れてはいけない。

また、当たり前ですが、テストを書いておくとエラーが検知しやすくなります。逆に言うと、エラーが検知しやすいテストをあらかじめ書いておくことが大切です。

5. 体系的な知識を身につける

公式ドキュメントや書籍などで、一度体系的な知識を身に着けておくといいと思います。

必要な知識をすべて覚えよう、ということではなく、ざっとでもいいので全体を舐めておくと、いざというときに問題のあたりをつけたり、調べて解決したりすることができるからです。体系的に理解しておくことで、「知っていればハマらなかったのに」と後悔することを減らし、本来時間をかけるべき箇所に集中できます。

何を学ぶにしても、付け焼き刃ではなくて、一歩深いところまで興味をもつことが大事だと思います。プログラマーの開発速度は「はまる」時間の長さで決まるで触れられている、「よくあるつくり」を意識するようなイメージです。

「最近ではXXというフレームワークがあって」というような表層の情報をたくさん集めることよりも、少なくても良いので、これは面白そうだ、というものについて深入りして、内部の構造や拡張ポイント、設計の指針などを理解する方が、速く美しくコードを書けるプログラマーになるための近道だと思うわけである。

ペアプログラミングして気がついた新人プログラマの成長を阻害する悪習にも、

知識についておいしいところをつまみ食いしようとしたり、文法やパラダイムについての理解を自分なりに整理できていない場合、知識ではなく、tipsになってしまい応用が利かず、無限にtipsを追い求めてしまう。

と書かれています。

6. 質問する

適切なタイミングで誰かに質問するのも重要なスキルです。質問は恥ではないし役に立つという記事によくまとまっていますが、個人的に重要だと思ったのは以下の点でした。

  • 質問のテンプレートを使う(どこまで自力でやればいいかを毎回考えずに済む)
  • 15分後も解決していなかったら必ず人に聞く(早すぎず遅すぎず)
  • 質問のレベルの把握
  • 経験のない仕事でも、世界のスタンダードがどうなっているのかを調べる

また、身の回りに、質問に関して知見のある人がいない、というときは、Stack OverflowやGitHubのissueで質問することができます。

その他

その他のテクニックとして、過去に出会って類似した問題と照らし合わせてあたりをつけることや、問題を言い換えたり簡略化したりして違った視点から見ることなどがあります。これらも、達人プログラマーによいリストがあるので抜粋します。

  • 要求を再解釈
    • もっと簡単な手段は存在するのか?
    • 本当の問題を解決しようとしているのか、それとも末端の技術的な問題にとらわれているのか?
    • なぜそれが問題なのか?
    • 解決を難しくしている真の原因は何なのか?
    • この手段でやり遂げなければならないのか?
    • そもそも解決しなければならない問題なのか?

問題解決のフローチャート

以上の問題解決の基本をふまえると、問題が生じたときの考え方の流れは次のフローチャートのとおり整理できます。

各項目の補足をしていきます。

1. 現象/再現手順を特定する

まずはじめに、現象/再現手順を特定することから始めます。これは、常に解決の計画を持つ問題を分割するために必要な手順です。当たり前のようですが、意識しないと忘れてしまいがちかなと思います。

例えば、ランダムに起こる現象(バグ)を、どういう状況で起こるのかを知らないままに調べていくのは危険です。もし調べるにしても、現象に当たりをつけるための調査という意識を持っておくといいかもしれません。

2. 原因を特定する

現象が分かったら原因を探ります。作業的には前手順「1. 現象/再現手順を特定する」とセットになることも多いのですが、そうではないケースもあるため、別の手順にしています。

まず大事なのは、問題を分割することです。コードのどの行がエラーの原因になっているのかまで絞り込むために、実装をコメントアウト等しながら切り分けていきます。問題が発生する前のコミットまで戻るのもひとつの手段です。できる限り余計な部分を排除した、最小構成から原因を探っていくのがいいと思います。

また、ググるスキルも重要です。自分は、以下のいずれかを組み合わせながら、もしくは単体ででググることが多い気がします(ちなみにまず最初に英語で調べます)。

  • エラーメッセージをそのままペースト
  • 現象/再現手順名
  • 原因と考えられる実装名

こういった指針をもっておくと、手順を実行していくだけで原因が特定できるので、イライラせずに問題を解決できることが多いです。

3. 解決策と思われるものを試す

調べた解決策を実装してみます。解決すればここで終了です。

4. 解決しない場合=>結果を残しておいて、また1のサイクルに戻る

解決しない場合、そのプロセスを文字に残しておくことが大事です。面倒に思うかもしれませんが、問題が複雑であるほど、結果を書いておくと早く切り分けできます。試した順番に因果関係がありそうな場合は、それをツリー構造で残しておくことも重要です。

まとめ

今回は、信頼できる書籍などをリソースを参考にして、ハマった時の問題解決の考え方を書いてみました。あくまで個人的な指針ではありますが、このような考え方のフレームワークを自分の中で持っておくと、様々な状況に対処しやすいのではないかと思っています。

自分もまだ模索中です。他に良い方法があればぜひ教えてください。

参考にしたリソース

Code Complete, Second Editionの抜粋

最後に、Code Complete, Second Editionの抜粋を紹介します。この記事で言いたいことが網羅されていると思います。

  • Techniques for Finding Defects
    • Use all the data available to make your hypothesis.
    • Refine the test cases that produce the error.
    • Exercise the code in your unit test suite.
    • Use available tools.
    • Reproduce the error several different ways.
    • Generate more data to generate more hypotheses.
    • Use the results of negative tests.
    • Brainstorm for possible hypotheses.
    • Keep a note pad by your desk, and make a list of things to try.
    • Narrow the suspicious region of the code.
    • Be suspicious of classes and routines that have had defects before.
    • Check code that's changed recently.
    • Expand the suspicious region of the code.
    • Integrate incrementally.
    • Check for common defects.
    • Talk to someone else about the problem.
    • Take a break from the problem.
    • Set a maximum time for quick and dirty debugging.
    • Make a list of brute-force techniques, and use them.
  • Techniques for Syntax Errors
    • Don't trust line numbers in compiler messages.
    • Don't trust compiler messages.
    • Don't trust the compiler's second message.
    • Divide and conquer.
    • Use a syntax-directed editor to find misplaced comments and quotation marks.
  • Techniques for Fixing Defects
    • Understand the problem before you fix it.
    • Understand the program, not just the problem.
    • Confirm the defect diagnosis.
    • Relax.
    • Save the original source code.
    • Fix the problem, not the symptom.
    • Change the code only for good reason.
    • Make one change at a time.
    • Check your fix.
    • Add a unit test that exposes the defect.
    • Look for similar defects.
    • General Approach to Debugging
    • Do you use debugging as an opportunity to learn more about your program, mistakes, code quality, and problem-solving approach?
    • Do you avoid the trial-and-error, superstitious approach to debugging?
    • Do you assume that errors are your fault?
    • Do you use the scientific method to stabilize intermittent errors?
    • Do you use the scientific method to find defects?
    • Rather than using the same approach every time, do you use several different techniques to find defects?
    • Do you verify that the fix is correct?
    • Do you use compiler warning messages, execution profiling, a test framework, scaffolding, and interactive debugging?