はじめに
プログラムを書いたことがある人なら、誰しもハマる
という状況に陥ったことがあると思います。ハマる
というのは、何かから抜け出せなくなってしまうことを意味しますが、開発の文脈では、ある問題やエラー、実装に対して、必要以上に多くの時間をかけてしまい、いつまでたっても解決の見込みが見えないことを意味することと思います。
今回は、そのハマった
時に、いかに早く脱却し、問題解決するかを書いていきたいと思います。
目次
フローチャート
時間がない人のために簡単なフローチャートをつくっているので、フローチャート詳細を御覧ください。
問題解決における6つの基本
図の詳細の前に、問題解決の基本的な考え方を書いておきます。当たり前だろ、と思うかもしれませんが、これが完璧にできているのであれば、何かにハマったりすることはそもそも稀、と意識しています。
もし、普段の実装で「同じことで丸1日悩んでいて解決の手立てがない」「これにさえ気付いていればもっとはやくできた」「単純なミスだった」みたいなことで時間を溶かしているのであれば、基礎的な思考ができていない可能性が高いかもしれません。
技術的な知識自体が時間のかかる原因となっている可能性はたしかにあります。しかし、適切な問題解決ができれば、初物で時間はかかったとしても、ハマる時間は減らせるのではないでしょうか(←なかなか理想通りにはいかないですが…)。
以下、問題解決において重要な基本を6つにわけて紹介しようと思います。
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のサイクルに戻る
解決しない場合、そのプロセスを文字に残しておくことが大事です。面倒に思うかもしれませんが、複雑な問題の場合、結果を書いておいたほうが早く切り分けがおわると思います。試した順番に因果関係がありそうな場合は、それをツリー構造で残しておくことも重要です。
まとめ
今回は、ハマった時にいかに脱却して問題解決するかを書いてみました。もともと、ほとんどが、信頼できるリソースからただ引用したものですが、あくまで、ここに書いたことは個人の指針でしかなく、こういったルーティンを自分の中で持っておくことが今後につながるのかなと考えています。
自分もいろいろ模索中なので…もし他にいいやり方があればぜひ教えてください。
参考
- 新人プログラマのうちに身に付けたい習慣、考え方(この半年で学んだことと反省) - Qiita
- プログラマたる者、人に頼る前にこれぐらいはやっておきたい - Qiita
- 新人エンジニアこそちゃんと調べてちゃんと知りちゃんと考える - Qiita
- ペアプログラミングして気がついた新人プログラマの成長を阻害する悪習 - Qiita
- 質問は恥ではないし役に立つ - Qiita
- バグなどの謎の現象に立ち向かうも闇が濃く、どうしても沼から脱出できない時に見るフローチャート
- プログラマーの開発速度は「はまる」時間の長さで決まる
- ソースコードって実際のところどういうふうに書いていますか?
- 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?