15年前の自分にしてあげたいプログラミングの助言
(mbuffett.com)ずっと自分の足を撃ち続けているなら、銃を直そう
- チーム内でシステムについてよく起きるミスがあるのに、そのミスを減らす方法を考えないことが多い
- こういう場合は、システムを改善してミスを減らすことが重要
- 経験談:
- iOS開発で CoreData を使うとき、UI の更新はメインスレッドでしかできない
- 購読コールバックがメインスレッドとバックグラウンドスレッドの両方で発生し、問題がよく起きていた
- 既存のチームメンバーはこれを認識してうまく対処していたが、新人メンバーのレビューでよく指摘されていた
- たまにミスが起きると、クラッシュレポートを見て
DispatchQueue.main.asyncを追加してきた - これを解決するため、購読レイヤーを更新して購読者をメインスレッドで呼び出すように変更した。所要時間はたった10分。
- クラッシュのクラス全体と、ちょっとした精神的負担を取り除けた
- 誰でも数分考えれば明らかな問題だったはず
- しかしこうした問題を解決する自然なタイミングがないため、不思議なくらい長く残り続ける
- つまり、こうした問題はチームに長くいるほど背景に溶け込みやすい
- 考え方を変える必要がある
- こうした問題があるときは、自分やチームの仕事をもっと楽にできることを時々思い出すべき
品質と速度のバランスを取る
- 実装速度と正確さへの自信のあいだには、常にトレードオフがある
- 今の状況でバグをリリースすることがどれくらい許容されるのか、自問してみるべき
- その答えが仕事の進め方に影響しないなら、硬直的すぎる
- 最初の職場でデータ処理プロジェクトをしていたときは、データをさかのぼって再処理できる優れた仕組みが整っていた
- バグを出しても影響はごく小さく、こういう環境ではガードレールにある程度頼って、もっと速く動ける
- 100% のテストカバレッジや大がかりな QA プロセスは、開発速度を落とすだけ
- 2社目では、数千万人が使う製品で高価値の金融データと個人識別情報を扱っており、バグは致命的だった
- 小さなバグでも事後分析が必要だった
- 機能のリリース速度はとても遅かったが、その年はバグを 0 件に抑えられたと思う
- ほとんどの場合、2社目のような状況にはいない
- バグが致命的でない状況(例: 99% の Web アプリ)では、速く出して速く直すほうがよい
- 最初から完璧な機能を出すのに時間をかけるより、もっと前に進める
のこぎりを研ぐ時間は、ほとんどいつも価値がある
- ツールをうまく使いこなすことは重要
- コードを素早く書けて、主要なショートカットを知っていて、OS やシェルに習熟しているべき
- リネーム、型定義への移動、参照の検索などは頻繁に使うことになる
- エディタの主要なショートカットをすべて把握し、自信を持って素早くタイピングできるべき
- ブラウザの開発者ツールを効果的に使うことも重要
- ツールを適切に選び、上手に使えることは大きな強み
- 新しいエンジニアに見られる最大のグリーンフラグの一つは、ツール選びとその習熟に関心があること
難しさを簡単に説明できないなら、それはたぶん偶発的複雑性であり、この問題は解く価値がある
- 私が最も好きだったマネージャーには、実装が難しいと主張するたびに、さらに深く問い詰めてくる癖があった
- 彼の返答はたいてい「それって X を Y するときに送るだけでは?」とか、「数か月前にやった Z と同じでは?」といったものだった
- それはかなり高いレベルからの反論で、私が説明しようとしていた実際の関数やクラスのレベルではなかった
- マネージャーがこうして単純化するのは、ただ鬱陶しいだけだという見方が一般的
- しかし驚くほど高い割合で、マネージャーが問い続けるうちに、自分が説明していた複雑さの大半が偶発的複雑性だと気づいた
- そして実際、それを先に解消することで、問題をマネージャーが言うように些細なものにできた
- このようなアプローチは、将来の変更も容易にする傾向がある
バグはもう一段深く解決しようとする
- バグを表面的に直すのではなく、根本原因を見つけて解決することが重要
- ダッシュボードに、現在ログイン中のユーザー状態から取得した
Userオブジェクトを扱う React コンポーネントがあるとき- Sentry で、レンダリング中に
userがnullだったというバグレポートが出るif (!user) return nullを素早く追加することもできるし
- あるいは少し調べると、ログアウト関数が 2 つの別々の状態更新を行っていると分かる
- 1 つ目はユーザーを
nullに設定し、2 つ目はホームページへリダイレクトすること
- 1 つ目はユーザーを
- この 2 つの順序を入れ替えれば、どのコンポーネントもこのバグを二度と踏まなくなる
- ダッシュボード内では、ユーザーオブジェクトは決して
nullにならないからだ
- Sentry で、レンダリング中に
- 1 つ目のタイプのバグ修正を続けると、システムはどんどん散らかっていくが、
2 つ目のタイプのバグ修正を続ければ、クリーンなシステムと不変条件への深い理解が得られる
バグ調査のために履歴を掘る価値を過小評価しないこと
- 私は
printlnやデバッガのような一般的なツールを使って奇妙な問題をデバッグするのがかなり得意だった - そのため、バグの履歴を追うために git をあまり見てこなかったが、ある種のバグではこれがとても重要
- 最近、サーバーでメモリが継続的にリークしているようで、OOM によって終了・再起動を繰り返す問題があった
- ありそうな原因はどれも排除されており、ローカルでは再現できなかった
- 目隠ししてダーツを投げているような気分だった
- コミット履歴を見たところ、Play Store の課金対応を追加した後から発生し始めていた
- ただ数件の HTTP リクエストにすぎず、百年考えても見る場所ではなかった
- 最初のアクセストークンが期限切れになったあと、アクセストークン取得の無限ループに陥っていたことが分かった
- 各リクエストで増えるメモリは 1kB 程度だったかもしれないが、複数スレッドで 10ms ごとにリトライすれば、あっという間に積み上がる
- 普通ならこういうことはスタックオーバーフローを引き起こしたはずだが、Rust で非同期再帰を使っていたため、スタックオーバーフローは起きなかった
- これは絶対に思いつかなかっただろうが、問題を起こしていると明らかな特定のコードを見に行けたことで、突然この仮説が浮かんだ
- いつこのアプローチを使うべきかというルールはない
- 直感に基づくもので、バグレポートを見たときの別種の「ん?」がこの種の調査を引き起こす
- 時間をかけてその直感は育てられるが、ときには非常に価値があると知っているだけでも十分
- 問題に合っているなら
git bisectを試してみよう- 壊れていると分かっているコミットと、正常だと分かっているコミットが 1 つずつあるなら
悪いコードはフィードバックをくれるが、完璧なコードはくれない。悪いコードを書く側に誤ろう
- ひどいコードを書くのは本当に簡単
- しかし、あらゆるベストプラクティスを完全に守るコードを書くのもまた本当に簡単
- ユニット、統合、fuzz、ミューテーションテストをすべて通す必要があるが、その前にスタートアップは資金が尽きるだろう
- プログラミングの多くはバランスを見つけること
- 速くコードを書く側に誤ると…
- ときには悪い技術的負債で痛い目を見ることになる
- 「データ処理には優れたテストを追加すべきだ」と学ぶことになる
- 後から直すのはしばしば不可能だから
- 「テーブル設計は本当によく考えるべきだ」と学ぶことにもなる
- ダウンタイムなしで変更するのは非常に難しいことがあるから
- 完璧なコードを書く側に誤ると…
- 何のフィードバックも得られない
- すべてが一様に時間のかかるものになる
- どこに時間を正しく使えていて、どこで無駄にしているのか分からない
- フィードバックの仕組みは学習に不可欠だが、それを得られていない
- 「悪い」コードの意味を明確にしておく
- 「ハッシュマップ生成の構文を思い出せなくて内部ループを 2 回使った」という意味ではない
- 意味するのは、たとえば次のようなこと:
- 特定の状態を表現不可能にするためにデータ収集を書き直す代わりに、いくつかの重要なチェックポイントに不変条件のアサーションを少し追加する
- サーバーモデルが書き込み用 DTO とまったく同じなので、そのままシリアライズする。大量のボイラープレートを書く代わりに、必要になった時点で後から DTO を作ればいい
- これらのコンポーネントは些細で、バグがあっても大きな問題にならないので、テストを書くのを省略する
デバッグしやすくする
- 長年にわたって、ソフトウェアをデバッグしやすくするための小さな工夫をたくさん身につけてきた
- デバッグを容易にする努力をしなければ、ソフトウェアがますます複雑になるにつれて、各問題のデバッグに膨大な時間を費やすことになる
- 変更するのが怖くなる。新しいバグをいくつか特定するだけで 1 週間かかるかもしれないから
- デバッグ時間のうち、セットアップ、再現、後片付けに使われる時間を意識して追跡しよう
- それが 50% を超えるなら、今回は少し長くかかっても、もっと簡単にできる方法を探すべき
- 他の条件が同じなら、バグ修正は時間がたつほど簡単になっていくべき
チームで働くなら、常に質問する
- 「すべてを自力で解決しようとすること」から「些細な質問で同僚を煩わせること」まで、スペクトラムがある
- キャリア初期の人の多くは、前者に寄りすぎていると思う
- 常に、コードベースにより長く関わっていたり、技術 X にずっと詳しかったり、製品をよりよく知っていたり、単に経験豊富だったりするエンジニアが周りにいる
- どこかで最初の 6 か月を働いていると、何かを理解するのに 1 時間以上無駄にしている一方で、数分で答えをもらえたはず、ということがよくある
- 質問しよう。質問が誰かにとって本当に厄介なのは、数分で自力で答えを見つけられたはずだと明白な場合だけ
デプロイのサイクルはとても重要。速く頻繁にデプロイできる方法を慎重に考えるべき
- スタートアップでは Runway が限られており、プロジェクトには締め切りがある
- 会社を辞めて独立すると、貯金は数か月しかもたないだろう
- 理想的には、プロジェクトの速度が時間とともに複利で増して、想像以上の速さで機能をデプロイできるようになる
- 速くデプロイするには多くのものが必要
- バグに弱くないシステム
- チーム間の素早いターンアラウンド
- 新機能の 10%(エンジニアリング時間の 50% を占める部分)を削る意思と、それがどこかを見抜く洞察
- 新しい画面・機能・エンドポイントへ組み合わせて使える、一貫した再利用可能なパターン
- 速くて簡単なデプロイ
- 速度を落とさないプロセス(不安定なテスト、遅い CI、厄介なリンター、遅い PR レビュー、宗教のように扱われる JIRA など)
- その他にも無数のこと
- 遅いデプロイは、本番を壊したのと同じくらい事後分析の対象にすべき
- 私たちの業界はそうは動いていないが、だからといって個人として「高速デプロイ」という北極星に従えないわけではない
6件のコメント
"自分の足を撃つ" = 自縄自縛
という意味ですか
何か問題のあるコード(壊れた銃)が問題を引き起こしているなら(自分の足を撃つ)、銃を直せということです。
まるで自分の頭の中をそのまま取り出して並べたような衝撃です……。
よく読みました!!
興味深く読みました。
開発者ではありませんが、共感できる部分が多いですね