🔍

【サンプル付き】プログラマーの問題解決手法

2024/12/02に公開

はじめに

フリーランスエンジニアの オーリア です。

「プログラマーの問題解決手法」と題して、エラーに対する問題解決アプローチをまとめました。これは筆者が実践しているプラクティスを体系化した記事であり、今まで読んだ本や先輩に教えてもらった内容を、改めて言語化したものです。

初心者〜中級者のプログラマーを対象としてまとめました。他にも、知識が好きな方には嬉しい内容になっているのではと感じます。また、私自身プログラマーとしてまだまだです。いわゆるベテランプログラマーの鋭い洞察・直感のようなものはありませんし、できないため、その言語化は含まれていません。

余談

この記事は、2022年にとあるイベントで登壇したときのスライドを元に、内容を肉付けしています。当時はまだプログラマーに転職して1年半しか経っておらず、今となってはよく偉そうに語れたものだなと思います。また、当時から概念整理や構造化が好きだったのだなと再認識しました。

いつか記事にしようと思っていて、2年半も経ってしまいました。ここで供養したいと思います。
当時の資料との差分として、LLM についての章を新しく追加しています。

問題解決手法の重要性

まず始めに、なぜ問題解決手法が重要なのかを説明します。

プログラマーは知的生産職

プログラミングは創造的な活動です。定形作業とは縁がないと言ってよく、まったく実装を行うことは基本的にありません。全く同じコードに見えたとしても、それは文脈の観点が抜け落ちており、機能・タスクとしては別物を実装しているはずです。

ゆえに、プログラマーは知的生産職です。毎回異なる問題に対し、自分の知識を使い、ときに補いながら、日々解決していきます。複雑な問題を分析し、有効なアプローチを考え、実践する必要があります。

プログラミングの大半は問題解決の時間

プログラミングの大半の時間は、問題解決であると考えています。書いたコードが1回で動くことは珍しいです。タイポのような小さなミスから、中々デバッグしづらいやっかいなバグまで、大抵は何かしらのエラーを解決する必要があります。エラー文を読んだり、原因がありそうなコードを読んだり、はたまたネットで調べたり、気づけばあっという間に時間が過ぎてしまった...そんな経験も多いと思います。

特に初めて触れたり、学ぶ内容の場合は顕著だと感じます。ライブラリの知識が浅いままでとりあえず手を動かし、動かない。理解はしているつもりだけど、特に厄介なケースにハマってしまった。新しいことを学ぶのを辞めればこんなこともないのかもしれませんが、技術は日々進歩しているし、それでは自分の成長が止まってしまいます。

問題解決と生産性

このように、プログラマーは知的生産職であり、プログラミングの大半の時間は問題解決です。問題解決の力量が生産性に直結します。プログラマーとしての生産性を上げるのであれば、問題解決を意識し、より良い手法で挑むことは、悪くない心がけだと考えています。

問題解決手法の一覧

私が特に有効だと感じた、エラーの問題解決手法をまとめます。

観察と思考

観察

まず、観察しましょう。

これはつまり、情報を集めるということです。プログラマーは知的生産職ですが、情報がないことには考えようがありません。何かしらの意味のある情報を得て、そこから原因を探っていきましょう。

ここで注意したいことは、無作為に情報を集めればよいということではありません。 後の「仮説と検証」で説明しますが、すべての情報を網羅しようとすると、時間がいくらあっても足りなくなってしまいます。

以下に、少ないコストで有効な情報を集める方法を、優先度が高い順に示します。

  1. エラー文を読む
    • エラー文は最も意味のある情報です
      • 適切なエラー設計がされていれば、大抵はエラー文に原因が書いてあります (特にライブラリのエラー)
    • すぐにネットで検索せず、じっくりとエラー文を読んでみることをおすすめします
      • エラー文は基本的に英語のため、意外と読み飛ばしがちです
      • 最初にエラー文は読むことは、状況を理解する大きな助けになります
  2. ログを読む
    • もう1つ、手を加えずに収集可能な情報として、ログがあります
      • ログには重要な情報が流されていることがあります
      • 時系列で情報を把握することにより、問題の輪郭を理解しやすくなります
  3. デバッグする
    • プリント文を入れたり、IDE でブレークポイントを設定したりして、デバッグします
    • ある状態のコードの状況を把握するのに役立ち、エラー文・ログからは得られない情報を取得できます
  4. 色々と試してみる
    • とにかく色々と試してみます
      • フロントエンドであれば、いろんなパターンを操作して、問題の輪郭を掴みます
      • バックエンドであれば、さまざまな入力値を試して、より多くの振る舞いを把握します
    • これは最終手段です
      • この方法は、素早く問題の輪郭を掴むのに、役立つときがあります
      • ただひたすらに、片っ端から色々と試して時間が過ぎている状態には、注意が必要です
        • 問題解決の手法として、闇雲な探索は最も生産性が低い行為です
        • より上位の観察手段や、他の問題解決手法を用いて、それでもわからないときに使用してください

思考

次に、状況を理解します。

いったい、どんな現象が起こっているのでしょうか?
何が起こっていて、何が起こっていないのでしょうか?

これにより、問題を具体化します。問題の解像度が上がり、問題を解決しやすくなります。特に後者の、何が起こっていないかの確認は、問題を理解する上で手助けとなることが多いです。

問題の理解

ここまでで、問題をより正確に理解できるようになります。この後に登場する、他の問題解決手段の精度が上がります。知的生産において、対象をどこまで正確に捉えることができるかは、とても重要なことだと考えています。

観察と思考、ぜひ試してみてください。
コツはすぐに調べようとせず、改めて問いを立てて自分の頭で考えてみることです。

分割統治法

問題をより小さな独立した問題に分解し、個別に対応します。

デカルト「困難は分割せよ」
ビル・ゲイツ「問題を切り分けろ」

これも問題解決の手段としてかなり有効なテクニックです。一見複雑そうに見えるが、よくよく見ると2個の独立した問題だった、というパターンがよくあります。

具体例

具体例を見てみます。
以下の関数と結果を見て、どのようなより小さな問題に分割できそうでしょうか?

const array = [1, 2, 3]
const newArray = double(array) // 値を2倍にする

console.log(newArray) // => [3, 6]

どうでしょうか。double という関数を通して値が2倍になるはずが、[3, 6] というなんとも言えない結果になっています。期待する結果は [2, 4, 6] です。

筆者は、この問題を分割しようとすると、以下の予測が立てられると考えました。

  1. 数字が2倍ではなく3倍される問題
  2. 配列の最後の要素が削除される問題

演習としてあえてややこしい問題にしましたが、感覚を少しでも共有できたら幸いです。

ぜひ「この問題は、より小さい問題に分割できないだろうか?」と考えてみてください。

仮説と検証

自分が最も生産性に寄与すると考えているテクニックです。

仮説を立て、検証します。これにより、より少ない時間で問題を解決することができます。

仮説がないとどうなるのでしょうか?答えは、情報収集に膨大な時間が消費されます。全ての情報を集めるのは無理です。情報が全て揃ってから、問題を解決するのでは、いつまで立っても問題は解決されません。情報は必要最低限にして、問題を解決する必要があります。

仮説とはなんでしょうか?それは、問題に対する「仮の答え」です。「暫定でこれが原因なのではないか」という、自分の中の答えが、仮説になります。

ではその仮説をどう使うのでしょうか?ここで検証が登場します。出した仮説を検証しましょう。例えば「この部分の処理が原因なのではないか」と仮説を出したとします。次にその仮説を検証する方法を考え、その仮説を検証するのに必要最低限の情報を集めます。その検証を実行し、仮説が当たっているか、外れているかを確認します。

場当たり的な対処を繰り返すことが、もっとも生産性が低い行為です。なんとなく目の前にあるコードをイジってみたり、とりあえず片っ端からデバッグしていくような行為です。これでは、もし現在見ているコードと原因のコードがはるかに遠かった場合、いつたどり着くか分かったものではありません。ここでも仮説を立てるとこで、ピンポイントに原因の近くにたどり着く確率が上がります。

具体例

分割統治法の例題を、ここでも考えていきます。

分割統治法では、以下の2つの問題に分割しました。それぞれについての仮説を考えてみてください。
いったい、何が原因と言えそうでしょうか?

  1. 数字が2倍ではなく3倍される問題
  2. 配列の最後の要素が削除される問題
const array = [1, 2, 3]
const newArray = double(array) // 値を2倍にする

console.log(newArray) // => [3, 6]

筆者は、以下のように考えました

  1. 数字が2倍ではなく3倍される問題
    • value * 2value * 3 になっているのでは?(仮説)
    • 実際のコードを確認する(検証)
  2. 配列の最後の要素が削除される問題
    • わからない、仮説がない

1の仮説と検証はまだしも、2の「わからない」とは何事かと思われたかもしれません。これは、実際には十分あり得るパターンです。仮説の精度は、その人の経験値や知識量に左右されます。 よって、その人の経験・知識量により、仮説を立てられないパターンが存在します。ここについては仕方がないので、落胆せずに他の手法を使いましょう。仮説と検証を用いて解決しようとする姿勢が重要だと考えています。

また、問題が複雑過ぎたり、得られる情報量が極端に少ないがために、仮説の立てようがないパターンもあります。こちらも仕方がないことが多いです。

この仮説と検証のサイクルを回すことにより、闇雲な問題解決から開放されます。
「仮説は何か?」「それを検証するには?」とぜひ考えてみてください。

仮説と検証については、以下の本がおすすめです。

仮説思考―BCG流 問題発見・解決の発想法

原因の切り分け

切り分け

原因が存在する範囲を切り分け、個別に探索します。

範囲分けする軸の例を、以下に示します。
空間軸と概念軸の例が似ていますが、ご容赦ください。

  • 時間軸
    • いつ、問題が発生したのか?
      • あれ、コードが動かなくなった
        • 昨日までは動いていたのに...
      • どこで動かなくなったのだろう
        • どのコミットで動かなくなったのかを特定する
        • 昨日〜今日まで、コミットを順番にロールバックしていく
  • 空間軸
    • どこに原因があるのか?
      • コードのどの場所に原因があるのか
        • フロントエンド・バックエンド・インフラのどこのコード?
        • Model・View・Controller のどこのコード?
  • 概念軸
    • どの要素に原因があるのか?
      • ページにアクセスすると502が表示される
        • ブラウザ・ミドルウェア・Applicationサーバーのどれに原因があるのか?

範囲を分け、1つ1つのより小さな範囲を確かめていくことによって、原因がない箇所を順番に潰していきます。

ラインの位置

範囲分けする際、どこでラインを引くかも重要になってきます。

  • 原因の見当がつくとき
    • 見当がつく箇所の近くで分ける
  • 原因の見当がつかないとき
    • 中央で半分に分ける

後者は、仮説と検証の考え方に似ています。見当立つかないとき、全体を 10:90 で分けてもしょうがありません。50:50 で分け、二分岐探索で効率よく原因を探っていきましょう。

具体例

具体例を考えていきます。例えば、以下のサーバーを構築したとします。

このとき、ページにアクセスするとエラー画面が表示されたとします。
どのように原因を切り分けられるでしょうか?

筆者は概念軸 (もしくは空間軸) で切り分けるのが良さそうと考えました。

  • どこに原因がありそうか?
    • ブラウザのリクエストが不正?
    • Nginx の設定が間違っている?
    • Rails サーバーが起動していない?

ここで、原因の見当がつかなければ、中央で切りましょう。

  • Nginx サーバーまで正常にリクエストが来ているか確認
    • NG → ブラウザ, Nginx に原因がある
    • OK → Nginx, Rails に原因がある

原因の切り分けも、非常に有効な手段でおすすめです。

ネットで調べる

Google 先生に聞きましょう。先人が残してくれた資産により、問題の内容から解決策まで、一発で見つかる可能性があります。

最近は、LLM に聞いてしまうことも多いかもしれませんね。

人に聞く

人に聞きます。これは最強の手段です。先人の知識・経験を間借りするおとができます。自分では解決できなくても、先人に聞けば一発で解決できることも多いです。実務では仕事を前に進めることが一番なので、活用することを恐れないようにしましょう。

余談ですが、相手への配慮も忘れないようにしたいです。事前に要点をまとめたり、結論から各論の流れで話すなど、心がけたいですね。

LLM に聞く

AI に聞きましょう。場合によっては、Google で検索するよりも速いこともあります。一般的な答え、中央値に近いような答えであれば、ほぼ完璧な回答を出してくれます。初歩的なエラーや小さいエラーは、どんどん AI に聞いていきましょう。

マインド

ここでマインドにも少し触れておきたいと思います。

間違っているのは人間

プログラムは書いたとおりに動きます。思ったとおりには動きません。
プログラムが思い通りに動かない場合、以下のどれかが間違っています。(体感、前2つが9割です)

  • 自分の認識
  • 自分が書いたコード
  • 自分が書いていないコード

ほとんどのエラーは解決可能です。自分の認識が間違っていれば、認識を改めればよいのです。自分が書いたコードが間違っていれば、コードを修正すれば済む話です。

常に自分のどこかが間違っているという前提に立ち、問題を解決していきましょう。

実演

実演に入りたいと思います。
記事の内容に実演も何もないですが、意味合いが近いのでそのまま実演とします。

内容は過去の発表資料と同じです。もう詳細を覚えていませんが、私が実際に遭遇したケースを扱います。掲載にあたり、適度に抽象化しています。

実演の内容が正解というわけではありません。みなさんも、実際に自分が遭遇したらどうするかを考えながら、読み進めてみてください。

状況

以下の HTML/JavaScript のコードがあります。

  • やりたいこと
    • 画面に span タグで foobar を表示させたい
    • foobar の間を改行させたい
  • 条件
    • 文字は JavaScript で後入れする
    • 1つの span タグのみを使用する
  • 初期実装
    • load イベントで後入れする
    • 改行コードで改行させる
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Title</title>
  </head>
  <body>
    <span id="text"></span>
  </body>
  <script>
    window.addEventListener('load', () => {
      const text = 'foo\nbar'
      const span = document.getElementById('text')
      span.textContent = text
    })
  </script>
</html>

準備は良いでしょうか。まずは、実際にこのコードを実行するとどうなるのか、見ていきましょう。

実行結果

ブラウザで表示させると、以下のようになりました。

あれ、改行されていませんね。
あなたなら、次の行動をどうしますか?

観察と思考

筆者の場合は、まず観察と思考を行い、問題をより正確に理解しようとしました。

  • 観察と思考
    • 情報を集める
    • 状況を理解する

手軽かつ意味のある情報として、ブラウザの検証ツールからDOMを見てみます。

一度情報が得られた段階で、改めて考えてみましょう。
これはいったいどのような状況でしょうか?

コードのおさらい
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Title</title>
  </head>
  <body>
    <span id="text"></span>
  </body>
  <script>
    window.addEventListener('load', () => {
      const text = 'foo\nbar'
      const span = document.getElementById('text')
      span.textContent = text
    })
  </script>
</html>

1つのテクニックとして、状況を言葉にし、問題をより具体化して考えてみましょう。
コードとDOMを見比べて、筆者は以下のように考えました。

  • 問題を具体化する
    • 「改行コード」が「半角スペース」になっている
      • 改行ではなく、改行コードである
      • 空白ではなく、半角スペースである
    • XXXになっている とは...
      • どこかで変換されているのか?
      • いったいどこで...?

問題を具体化し、「改行コード」が「半角スペース」になっている としました。後半の ...になっている の部分が曖昧ですが、変換されている と断定するのも尚早と感じました。

ここから取れる次のアクションはなんでしょうか?

原因の切り分け

筆者は、原因を切り分けたいと考えました。

  • 原因の切り分け
    • いったどこに原因があるのか?
コードと結果のおさらい
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Title</title>
  </head>
  <body>
    <span id="text"></span>
  </body>
  <script>
    window.addEventListener('load', () => {
      const text = 'foo\nbar'
      const span = document.getElementById('text')
      span.textContent = text
    })
  </script>
</html>

どこに原因がありそうか、以下のように考えました。

  • どこかに原因がある?
    • 書いたコード
    • JavaScriptの実行
    • HTMLの表示
  • 現状を踏まえると
    • エディタ上では、確かに foo\nbar とコードを入力している
    • エディタ上の値は問題なさそうなので、代入後に何かおかしいことが起こっているのだろうか?
      • JavaScript の動作か HTML の表示が怪しいのでは
      • 代入後と代入前で切り分けてみる

JavaScript による、HTML 要素の値の代入が怪しいと考えました。(span.textContent = text の部分)
そこを、範囲を切り分けるラインとします。代入後の範囲を検証するために、代入後の値をログに出力してみます。

 <!DOCTYPE html>
 <html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Title</title>
  </head>
  <body>
    <span id="text"></span>
  </body>
  <script>
    window.addEventListener('load', () => {
      const text = 'foo\nbar'
      const span = document.getElementById('text')
      span.textContent = text
+
+     console.log(span.textContent)
    })
  </script>
 </html>

すると、以下のような出力となりました。

代入後の値は、改行コード付きで格納されていそうですね。ということは、JavaScript のコードではなく、HTML の表示側に問題があるのでしょうか。

さて、次はどうしましょうか...?

仮説と検証

筆者は、このタイミングで一度仮説を立てたいと考えました。
コストがかからず有効な情報は集め終わったたため、仮説を立て原因をよく調べたいと考えています。

  • 仮説と検証
    • 原因には何がありえそうか?

仮の答えとして、どのような原因がありえそうか、以下のように考えました。

  • 情報は集めたが...しかし、何が原因かわからない
    • 仮説が「ない」状態
    • 自分は答えを持っていない

ここでも何事かと言われそうですが、仮説を立てることができませんでした。

手早く有効な情報を集め、仮説はない。
さて、どうしましょうか?

ネットで調べる

ネットで検索してみようと思いました。
自分の中に答えがないことがわかったため、自分の外に答えを探しに行きます。

「span 改行コード スペース」で検索してみました。すると、以下の記事がヒットします。

インライン要素に改行いれたら、予期せぬ空白が入ってしまった!

内容をまとめてみます。

  • HTMLでは、改行コードは空白として処理される
    • CSSの white-space が初期値の場合、「空白、タブ、改行」は空白と解釈される
    • つまり white-space の値が原因か?

これは「自分の認識」が間違っていたパターンですね。改行コードを挿入すれば、HTML で改行されると認識していました。

さて、次のアクションはどうでしょうか。

仮説と検証

新しい情報を得たことで、仮説を立てることができそうです。

  • 仮説は何か
    • white-space の値を変えることで、改行が表示される」ことがありえそう
  • 検証する
    • CSS を追加し、実際に white-space を設定してみる

実際に検証してみましょう。

 <!DOCTYPE html>
 <html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Title</title>
  </head>
+ <style>
+   .text {
+       white-space: pre;
+   }
+ </style>
  <body>
    <span id="text" class="text"></span>
  </body>
  <script>
    window.addEventListener('load', () => {
      const text = 'foo\nbar'
      const p = document.getElementById('text')
      p.textContent = text
    })
  </script>
 </html>

結果を見てみると...

無事に改行されましたね!🎉

まとめ

実演の内容を振り返ってみると、以下の流れとなっていました。

  1. 観察と思考
    • 情報を得る
    • 問題を理解する
  2. 原因の切り分け
    • 原因を切り分ける
    • 情報を得た
  3. 仮説と検証
    • 仮説が立てられない
  4. ネットで調べる
    • 情報を得る
    • 自分の認識が間違っていた
  5. 仮説と検証
    • 仮説を立てる
    • 検証する

どうでしょうか。結果としては、ネットで調べた情報で解決しており、最初からネットで調べておけばダイレクトに解決できる問題でした。しかし、検索ワードには span 改行コード スペース と精度の高い言葉を指定できており、それは最初の「観察と思考」で問題を詳細に理解したからでした。また、頻繁に「仮説と検証」を実践することにより、必要最低限の情報収集で解決できたのではないかと感じます。実際にも、ネットの検索は一度しか行っておらず、それ以外の情報収集はローカルですぐ試せる範囲に収まりました。

今回は「自分の認識が間違っていた」パターンでした。このパターンは非常に厄介です。 思い込みにより、自分の認識が間違っていることに気づきづらく、時間を浪費しがちです。ここでも体系だった解決アプローチを取ることにより、むやみな時間の浪費を最小限に抑えることができます。

終わりに

いかがでしたでしょうか。元の資料にアウトラインは合ったものの、気合が入りすぎて大作となってしまいました。

言葉遣いが AI チックになっていると感じるかもしれませんが、たまたまです。文章を書くことはある程度慣れているつもりでしたが、外部へ発信する記事となると難しいですね。記事を書く上で、1文ごとに改行するのか、段落でまとめるのかも難しいです。

私自身、生産性が好きで、理論が大好きです。集中力が切れると、つい無作為な問題解決をしていしまいますが、常に構造化された手法を意識したいと感じました。その方がより速く、またベテランプログラマーのような直感・経験がなくとも、生産性を高められると信じています。

ここまで読んでくださりありがとうございました。少しでも新しい発見になりましたら幸いです。

参考文献

Discussion