へっぽこびんぼう野郎のnewbie日記

けろけーろ(´・ω・`)! #vZkt8fc6J

ぼくが何を考えてコードの修正をやるのかをとりあえず適当に書いてみたよ

他の人のブログを読んで、自分の中で思っていたことを整理したくなったので書いてみました。

パラグラフなど考えずに、思いついたものを文章の流れの中に差し込んでいるだけなので、箇条書きやTip集みたいになっていて、コンテキストが頻繁に変わるので読みにくくなっていると思います。あらかじめご了承くださいm( )m 骨子は概要のとおりです。

概要

何が問題かを把握する
→ 原因がどこかを特定する
→ どう解決するのか策を練る
→ 実際に解決する
→ 同じ原因で起こっている別の箇所にある問題を特定する

1. 何が問題かを把握する

「何が問題か」を、文で正確に表現できるまで切り分ける。

画面Xで、ユーザーAが、条件Cで、ボタンTを押したときに、Pが発生するべきであるが、なぜか、ときどきQが発生してしまう

他人が絡む場合は、ここで確認を取る。自分の勘違いで違う場所を試していたり、仕様だったりすることもある。バグ報告が実態と違っていて再現できないこともある。

確認ができたら問題を分割していく。文のうち、どこの箇所が削除可能かを考えて、これ以上削れないというところまで試していく。

分割は超重要。考えるべきことが大きく減るので絶対にやるべき。この分割の部分は、複雑になればなるほど、集合論の知識が役に立つ。

確認も重要。確認不足のせいで「あとあと考えると、そもそも問題ではなかったこと」のための無意味な実装をする場合もある。問題を正確に把握することはかなり重要

「なぜそれが問題なのか」も共有しなければ、実際は潜在的な問題が別の箇所にあるにも関わらず、この問題だけを解決するための次のフェイズに進むことになる。

問題把握が正確な場合、この段階でそもそもコードを1行も書かずにみんなが幸せに終わることも多い。

よくよく聞いてみると運用次第で実現できるとか、予算や工数的に確実に赤字になる無謀なチャレンジになるとか、そもそも顧客の勘違いだったとか、簡単にできればやってほしいだけで、キツそうならやらなくていい案件だったとか、バグじゃなくて仕様だったとか。

なので、炎上の予防としてもかなり有用。

コードをごにょるのだけが解決策ではないというのを念頭に置く。

分割は↓みたいな感じでやる。

ユーザーAが、条件Cで、ボタンTを押したときに、Pが発生するべきであるが、なぜか、ときどきQが発生してしまう

 ↓

どのユーザーでも、条件Cで、ボタンTを押したときに、Pが発生するべきであるが、なぜか、ときどきQが発生してしまう

 ↓

条件がCでない場合で、ボタンTを押したときは、全くQが発生しない

 ↓

どのユーザーでも、条件Cで、ボタンTを押したときに、Pが発生するべきであるが、なぜか、ときどきQが発生してしまう

ここまで問題を分割することができた。分割できてはじめて、原因を特定し始める。

2. 原因がどこかを特定する

「原因は絶対これだ!」と早とちりしない。

ときどき「これかもしれない!!!」とピンと来るときもあるが、そういうときも、いきなり解決策を書くのではなく、冷静に「なぜ原因がこれであるのか」という証明をする。

そうしないと、気のせいとか夢を見ていたとか、見ていた画面が開発画面じゃなかったとか、おバカな理由で、治ったことになってしまう場合がある。

ピンときたあとは「解決のときが近い!」と感じがちだけど、そのまま突き進むとドツボにはまる。おとなしくピンと来る前の気持ちに戻って原因を特定する作業を続行する。

何もわからない砂漠のような場所でウロウロしていると、ちょっと緑色のなにかが見えただけで「オアシスだ!!」と感じてしまい、そして「さっきここに緑色の何かがあったからきっとここがオアシスだ!」と考えて、オアシスではない場所で水を探しつづける羽目になる。実際にはずっと砂漠なのに。

これかもしれない!!!!!あるぇ?違ったぞ……じゃあこれか!!!!?違う??なんで!!これか??!!!じゃあこっちは!!!?これかああああ!違うの!?ナンデ!?これかあぁぁああああ?アアアアアアアアー

という手段でもたどり着くときはあるし、こっちのほうがたどり着くのが早いこともある。ただしたまにどれだけ時間をかけても一歩も進んでいないことがある。これをハマるという。個人的にハマるべくしてハマったハマりと考えている。

原因を特定するためには、コードを探索することが肝で、幻影や誘惑に惑わされず、「ここまでは正しい」ことを確認し続けて探索したり、ひたすらコードを追う作業を行う。

「これがこうなっているはずだから確認は不要」というような考えは捨てて、ひとつひとつバカみたいに確認して、範囲を狭めていく。結局ここでも問題を分割していく。

これは明らかにこうなっているはず。実行するまでもないね。まぁとりあえず実行してみるか……ほら、こうなって……なってねぇえええええじゃああああん

ということが死ぬほど多い。自分も他人も信じず、ただ機械だけを信じることが必要。

問題が判明するまでは、「解決しよう」という気持ちは完全に捨てて、まず「いったいどこで問題が起こっているのか」を探る。解決したい気持ちが先行すると、これを見落とすことが多い。

調べ終わると、以下のような文章が書けることが多い。

「条件 f=True」のときかつ「ボタンTのonClickイベントが発火した」とき、内部のクロージャが呼ばれて、クロージャ内部にある実装の甘い非同期処理によってときどき canGenerateData=False となり、そのため後続の処理によってQが発生する

 ↓

「非同期処理の実装が甘かった」理由は、複雑な非同期処理を行うための変数が足りず、変数を外部から持ってこようとすると、コードの複雑度が上がるため。平たく言えば、場当たり的な非同期処理のため。

ライブラリの中も追うと原因が特定しやすくなってべんり。勉強にもなる。

知識が不足していると、実際には原因でないものを「これが原因だ」と思い込んで、次のフェイズに進んでしまうことがあるので「自分の知らない知識がある」ことを前提にことを進める。自分の知識の範囲の中だけで考えないようにする。

3. どう解決するのか策を練る

原因を特定できてすぐに解決策が見つかることもあるが、そうでないことの方が多い。

「コードの構造がおかしいのでこれを変更しなければいけない」「変数名が混乱を来した主原因だったので変数名を変更する必要がある」などなど。

「そもそも仕様がやばいこと」もある。「このままやっても理論的に不可能なこと」もある。そうなると実際の解決までめちゃくちゃ遠い。場当たり的な解決になることも稀ではない。

解決策を考えるフェイズでは、今度は考える範囲を狭めるのではなく、増やしていく。いろんなコードを見る。コードの外も見る。マネージャサイドや営業サイドに解決策が眠っていることもある。

そもそも非同期処理である必要がなかったので非同期処理をやめた。非同期処理である必要がない理由は、単に非同期処理やってみたかっただけだったから。

影響範囲もちゃんと考える。修正はなるべく小さくする。小さくできない場合は、かなり大きな問題が眠っている。

影響範囲を考えないと、「この問題」を解決した代償として別の問題が何個も発生するといったことが起きる。そうなるともう、もぐらたたきみたいな感じ。

非同期処理のスコープはかなり小さく、処理の遅延も無いため同期処理にしても問題は発生しない。発生しないことも確認した。

原因を特定するのは簡単でも、解決策を探るのはとてもむずかしい。「こっちを立てればあっちが立たず」のようなことが多い。

「バグの直接的な原因」は簡単でも、「バグが出るコードが書かれてしまった原因」を解決するのは根本的な部分なので、極めて難しいし見えにくい。解決方法も調べて出てくるものではない。

ここはもう広範囲に勉強して、実践して、発見できるようにするしかないと思う。

4. 実際に解決する

ここまでのやり方が全く見当違いなこともある。

実際に解決する段階で、アクシデントがあまりにも多い場合、手順にミスがあることが多い。まだまだ原因を特定する必要がある。

時間や人的資源の不足や、知識の欠如によって「本当は、真に解決はできていないけど、今はこうしなければならなかった」で終わることも多々ある。

これは「負けた」と考えるべきで、「まぁいいか」と考えてしまうと、「まぁいいか」が積み重なって「ひどいプログラム」が完成する。

5. 同じ原因で起こっている別の箇所にある問題を特定する

同じ轍は踏まない。

だいたい発生した原因と似たようなコードが別の場所にある。コピペだったり色々。

だって今まで「そう書くのはダメだ」って気付いてなかったんだから、他の箇所もそうなってるに決まってる。

これは原因から再現させられるので、別のバグを比較的すぐに発見できる。ここまでの作業よりは遥かに容易い仕事だ。

そのバグを直すかどうかはまた別の話だが、どれだけあるかを洗い出しておく。でないと後で忘れるため。

また、この原因を訓戒とする。

今後はコードレビューで予防する。これで少なくとも技術的負債の利息は増えなくなる。

おわりに

まとめると

問題見つける → 原因見つける → 解決策見つける → 解決する → 教訓にする

というだけの1行で「なぁんだ。当たり前のことじゃないか」として終わることだけど、その1つ1つがどれも重いタスクなので、1つ1つに真摯に向き合って、いつも心に秘めておいて、自分に問いかける必要がある。

だいたいそんなふうに考えるとうまくいくことが多い。

ただ、原因を見つけるのがつらすぎたり、解決策が浮かびにくいと、どうしても悪い方向の誘惑に負けてしまう。

深追いするための知識が足りてないと「あーつらい!こことかここを試したらなんかうまくいかないものか……頼むぅ頼むぅ直ってくれ〜〜〜🙏」と悪魔に祈りがちになる。

落ち着いて「急がば回れ」の精神で、楽そうに見える遠回りな道を選んでたどり着けるかどうかを試すのではなく、絶対にたどり着くであろう茨の道を辛抱してゆっくり着実に道を作りながら進む方が最終的に早く解決する。

ただし短期的には解決が遅くなる場合が多いので、ついつい楽そうな道を選んで何個も "解決" したくなることがある。そこに気をつける。真の茨の道は、一見楽そうに見える道なのだ。

逆に、現実的な問題として、いつでも茨の道を選ぶことが正解というわけでもない。ここに難しさがある。ただ個人的にはぼくは茨の道を選んでいきたい。「昨日の敵は今日の友」とも言うし、茨の道も次の日には高速道路になる。

解決方法の模索や原因調査の段階で「膨大な基礎知識が要求されること」は多い。知らないことは考えられない。たとえばプロファイラという言葉を知らなければプロファイラは動かせないしプロファイラ使おうという発想も浮かばない。

そういうとき、あらかじめの勉強や実践がものを言う。なので「知識はないけどこの考え方さえあれば大丈夫さ」というわけでは全然ない。知識がないとわけわからないことが多すぎてひたすらつらすぎるだけだ。そうして悪魔の誘惑に負けてハマる。

また、知識不足のせいで次の一手をどう取ればいいのかわからなくなったとき、ぼくは詰んだと表現することが多い。ハマりはしないけど詰む。そして新たな問題が噴出する。ここに来るとコードの問題ではなかったりする。

なぜ詰んだのかは、詰んだ場所からは自明でないことが多い。そして「なぜここで自分は詰んでいるのか」という問いを得る。小問題のはじまりだ。そしてタイトルに再帰する。

そんな感じで考えています。