本日は人生の数ある選択肢のなかから、こちらのブログを読むという行動を選んでくださいまして、まことにありがとうございます。
はじめに
正直に言えば、プログラミングのルール本には懐疑的だった。「Clean Code」も「Effective Java」も読んだ。読んだが、現場でそのまま使えたことは少ない。コンテキストが違う。チームが違う。言語が違う。ルールは理想であり、現実は常に例外だ。
そう思っていた。本書を読むまでは。
Chris Zimmerman氏の「The Rules of Programming」(邦題:ルールズ・オブ・プログラミング ―より良いコードを書くための21のルール)は、私の予想を裏切った。これは「ルールを守れ」という本ではない。「ルールの本質を理解し、現場に合わせて判断しろ」という本だ。著者自身が、読者にこれらのアプローチを鵜呑みにするなと言っている。この謙虚さが、逆にこの本の信頼性を高めている。
本書は、大ヒットゲーム『Ghost of Tsushima』などで知られるゲーム制作スタジオ、Sucker Punch Productionsの共同創設者であるChris Zimmerman氏によって書かれた。ゲーム開発の本だが、コードの品質、パフォーマンス、保守性に関する原則は、どの分野でも共通している。「動くコードは書ける。でも、もっと良いコードがあるはずだ」——そう感じている人に読んでほしい。
本日は #英語デー🌏
— プレイステーション公式 (@PlayStation_jp) 2021年4月23日
あの名台詞、英語で言ってみよう!
"誉れは浜で死にました。
ハーンの首をとるために。"
"Honor died on the beach.
Khan deserves to suffer."
- 境井仁 (『Ghost of Tsushima』より)#ゴーストオブツシマ #GhostofTsushima #英語の日 #ゲームで学ぶ英会話 pic.twitter.com/RBYRuRVmvx
ブログのタイトルは「誉れは浜で死にました。」- 境井仁 (『Ghost of Tsushima』より)からいただきました。このタイトルは、本書の内容と呼応するように、時に固定観念や既存のルールを疑い、現場の状況に応じて柔軟に対応することの重要性を示唆しています。
21のルールの意義と特徴
21のルールは、単なるベストプラクティス集ではない。例えば、「コードは書くものではなく、読むものである」というルール。言葉にすれば当たり前だが、これを本当に理解しているかどうかで、書くコードは変わる。
ルールは現場で死にました
私はこのタイトルを皮肉として付けたのではない。これは事実だ。
10年近くプラットフォームエンジニアリングとSREに携わってきて、何度も見てきた。「DRYを徹底しろ」と言われて過度に抽象化されたコード。「テストカバレッジ80%以上」と言われて意味のないテストが量産される現場。ルールは、それ自体が目的になった瞬間に死ぬ。
本書が違うのは、著者自身がルールの限界を認めている点だ。25年間同じプロジェクトに携わってきた著者が「これは私たちのやり方であり、あなたの現場では違うかもしれない」と繰り返し言う。この謙虚さが、逆説的に21のルールすべてに説得力を与えている。
本書を読んで気づいたのは、良いコードとは「ルールに従ったコード」ではなく「ルールの本質を理解した上で判断されたコード」だということだ。これは単なる技術論ではない。プログラマーとしての判断力の問題だ。
当初の目論見と能力不足による断念
当初、様々なコーディングルールをまとめて紹介しようと考えていました。Clean Code、Effective Java、The Pragmatic Programmer、UNIX哲学——30冊ほどリストアップして、3週間で挫折しました。ルールをまとめるルールが必要だったのかもしれません。あるいは、Rule 17が言うように「大きな問題を解け」ではなく「身の丈に合った問題を解け」という別のルールが必要だったのかもしれません。
この経験から学んだのは、良質な情報をキュレーションすることの難しさです。今後、機会を見つけて他のコーディングルールについても順次紹介していきたいと考えています。挫折しなければ。
日本語版
日本語版が出た。英語版を先に読んでいたが、日本語で読み直すと「あ、そういう意味だったのか」と腑に落ちる箇所がいくつかあった。
執筆について
この記事の執筆にはLLMを活用している。主張と判断は私のものだが、文章の構成にはAIの力を借りた。議論や意見交換はXのDMで歓迎する。
本編
21のルールの構造:3つの層
本書の21ルールを読み進めていて気づいたのは、これらが単なる羅列ではなく、3つの層に分類できるということだ。
第1層:コードの品質(What is good code?) Rule 1(シンプルさ)、Rule 3(命名)、Rule 7(失敗ケースの排除)、Rule 9(崩壊性)、Rule 15(雑草取り)、Rule 18(自己説明的コード)
第2層:プロセスの品質(How to maintain quality?) Rule 2(バグの伝染性)、Rule 6(コードレビュー)、Rule 8(動いていないコード)、Rule 12(規約)、Rule 13(デバッグ)、Rule 19(並行リワーク)、Rule 21(地道な作業)
第3層:思考の品質(How to make decisions?) Rule 4(一般化)、Rule 5(最適化)、Rule 10(複雑さの局所化)、Rule 11(2倍良くなるか)、Rule 14(4つの味)、Rule 16(結果から逆算)、Rule 17(大きな問題)、Rule 20(計算せよ)
この分類が重要なのは、層によって「死因」が異なるからだ。
- 第1層の死因:技術的殺人——「パフォーマンス要件のため」「メモリ制約のため」。犯人は明確で、証拠も残る。
- 第2層の死因:組織的過失致死——「締め切りのため」「人員不足のため」。犯人は不明で、全員にアリバイがある。
- 第3層の死因:自殺——そもそも適用すべき状況を誤った。自分で自分を殺した。
検死報告書を書くと、だいたいこの3パターンに分類される。私の経験上、最も多いのは第2層だ。そして、最も反省しないのも第2層だ。
以下、各ルールを見ていこう。本書の主張を紹介しつつ、私自身の経験から「このルールが機能した場面」と「このルールが死んだ場面」を交えて考察する。
Rule 1. As Simple as Possible, but No Simpler
第1章「As Simple as Possible, but No Simpler」は、プログラミングの根幹を成す重要な原則を探求しています。この章では、シンプルさの重要性、複雑さとの戦い、そして適切なバランスを見出すことの難しさについて深く掘り下げています。著者は、ある言葉を引用しながら、プログラミングにおける「シンプルさ」の本質を明確に示しています。
この主題に関しては、「A Philosophy of Software Design」も優れた洞察を提供しています。以下のプレゼンテーションは、その概要を30分で理解できるよう要約したものです。
両書を併せて読むことで、ソフトウェア設計におけるシンプルさの重要性をより深く理解することができるでしょう。
シンプルさの定義と重要性
著者は、シンプルさを「問題のすべての要件を満たす最もシンプルな実装方法」と定義しています。この定義は、一見単純に見えますが、実際のソフトウェア開発において深い意味を持ちます。シンプルさは、コードの可読性、保守性、そして最終的にはプロジェクトの長期的な成功に直結する要素だと著者は主張しています。
実際の開発現場では、この原則を適用するのは容易ではありません。例えば、新機能の追加や既存機能の拡張を行う際に、コードの複雑さが増すことは避けられません。しかし、著者が強調するのは、その複雑さを最小限に抑えることの重要性です。これは、単に「短いコードを書く」ということではなく、問題の本質を理解し、それに最適なアプローチを選択することを意味します。
複雑さとの戦い
著者は、プログラミングを「複雑さとの継続的な戦い」と表現しています。この見方は、多くの経験豊富な開発者の実感と一致するでしょう。新機能の追加や既存機能の修正が、システム全体の複雑さを増大させ、結果として開発速度の低下や品質の低下につながるという現象は、多くのプロジェクトで見られます。
著者は、この複雑さの増大を「イベントホライズン」に例えています。これは、一歩進むごとに新たな問題が生まれ、実質的な進歩が不可能になる状態を指します。この状態を避けるためには、常にシンプルさを意識し、複雑さの増大を最小限に抑える努力が必要です。
シンプルさの測定
シンプルさを測る方法について、著者はいくつかの観点を提示しています。
- コードの理解のしやすさ
- コードの作成の容易さ
- コードの量
- 導入される新しい概念の数
- 説明に要する時間
これらの観点は、実際の開発現場でも有用な指標となります。例えば、コードレビューの際に、これらの観点を基準として用いることで、より客観的な評価が可能になります。
シンプルさと正確さのバランス
著者は、シンプルさを追求する一方で、問題の要件を満たすことの重要性も強調しています。この点は特に重要で、単純に「シンプルなコード」を書くことが目的ではなく、問題を正確に解決しつつ、可能な限りシンプルな実装を目指すべきだということを意味します。
例として、著者は階段の昇り方のパターン数を計算する問題を取り上げています。この問題に対して、再帰的な解法、メモ化を用いた解法、動的計画法を用いた解法など、複数のアプローチを示しています。各アプローチの利点と欠点を比較することで、シンプルさと性能のトレードオフを具体的に示しています。
コードの重複とシンプルさ
著者は、コードの重複を避けることが必ずしもシンプルさにつながるわけではないという興味深い観点を提示しています。小規模な重複は、時としてコードの可読性を高め、理解を容易にする場合があるという主張は、多くの開発者にとって新鮮な視点かもしれません。
この主張は、DRY(Don't Repeat Yourself)原則と一見矛盾するように見えますが、著者の意図は、原則を盲目的に適用するのではなく、状況に応じて適切な判断を下すべきだということです。小規模な重複を許容することで、コードの全体的な構造がシンプルになり、理解しやすくなる場合があるという指摘は、実務的な視点から重要です。
まとめ:このルールが死ぬとき
著者の主張は正しい。シンプルさの追求は、プロジェクトの成功に直結する。しかし、このルールが現場で「死ぬ」瞬間を私は何度も見てきた。
シンプルさが死んだ現場その1:パフォーマンス要件 あるAPIゲートウェイの開発で、最初はシンプルな同期処理で実装した。コードは読みやすく、理解しやすかった。しかしレイテンシ要件50ms以下という制約の前に、このシンプルさは死んだ。非同期処理、コネクションプール、キャッシュ層と、複雑さを受け入れざるを得なかった。
シンプルさが死んだ現場その2:「将来の拡張性」という呪文 逆のパターンもある。「将来、他のクラウドプロバイダにも対応するかもしれない」という一言で、シンプルなAWS直接呼び出しが、抽象化レイヤーの山に埋もれた。その「将来」は3年経っても来なかった。過度なシンプルさへの警戒が、過度な複雑さを生む。
このルールの適用限界 - レイテンシ・スループット要件が厳しい場合、シンプルさより効率を優先せざるを得ない - チームの習熟度が低い技術では、「シンプル」の定義自体が変わる - レガシーシステムとの統合では、「シンプルな新設計」より「複雑だが互換性のある設計」が正解になる
シンプルさは目的ではなく手段だ。「このコードはシンプルか?」ではなく「このコードは、この状況で最適な複雑さか?」と問うべきだ。
Rule 2. Bugs Are Contagious
第2章「Bugs Are Contagious」は、ソフトウェア開発における重要な課題の一つであるバグの性質と、その対処法について深く掘り下げています。著者は、バグが単なる孤立した問題ではなく、システム全体に影響を及ぼす「伝染性」を持つという洞察を提示しています。この章を通じて、バグの早期発見と対処の重要性、そしてそれを実現するための具体的な方法論が示されています。
完全な余談なのですがこの章の内容は、一見「割れ窓理論」を想起させますが、最近の研究ではこの理論の妥当性に疑問が投げかけられています。例えば、「Science Fictions あなたが知らない科学の真実」では、有名な科学実験の再検証だけでなく、科学研究の制度的な問題点や改善策についても論じられています。
この書籍は、科学研究の信頼性向上のための追試制度の提案や査読プロセスの改善など、建設的な内容を含んでおり、科学的知見の批判的検討の重要性を示唆しています。「割れ窓理論」は本書では直接言及されていませんが、同様に再検証が必要とされる理論の一つとして考えられています。例えで出したら後輩に指摘されてしまうかもしれません。
バグの伝染性
著者は、バグが存在すると、他の開発者が意図せずにそのバグに依存したコードを書いてしまう可能性があると指摘しています。これは、バグが単に局所的な問題ではなく、システム全体に影響を及ぼす「伝染性」を持つことを意味します。例えば、あるモジュールのバグが、そのモジュールを利用する他の部分にも影響を与え、結果として複数の箇所で問題が発生するという状況です。
この洞察は、日々の開発現場でも当てはまるものです。例えば、APIの仕様にバグがあると、それを利用する多くのクライアントコードが影響を受けることがあります。そのため、バグの早期発見と修正が極めて重要になります。
早期発見の重要性
著者は、バグを早期に発見することの重要性を強調しています。バグが長期間放置されるほど、それに依存したコードが増え、修正が困難になるというわけです。これは、多くの開発者が経験的に知っていることかもしれませんが、著者はこれを「entanglement(絡み合い)」という概念で説明しています。
実際の開発現場では、この「entanglement」の問題は頻繁に発生します。例えば、あるライブラリのバグを修正したら、それを使用していた多くのアプリケーションが動かなくなるという事態は珍しくありません。これは、アプリケーションがバグの振る舞いに依存していたためです。
自動テストの重要性
著者は、バグの早期発見のための主要な手段として、自動テストの重要性を強調しています。継続的な自動テストを行うことで、バグを早期に発見し、「entanglement」の問題を最小限に抑えることができるというわけです。
しかし、著者も認めているように、自動テストの導入には課題もあります。例えば、ゲーム開発のような主観的な要素が大きい分野では、すべての要素を自動テストでカバーすることは困難です。また、テストの作成自体にも多くの時間とリソースが必要になります。
ステートレスコードの利点
著者は、テストを容易にするための一つの方法として、ステートレスなコードの作成を推奨しています。ステートを持たない純粋な関数は、入力に対して常に同じ出力を返すため、テストが容易になります。
これは、実際の開発現場でも有効な方法です。例えば、以下のようなRustのコードを考えてみます。
fn sum_vector(values: &[i32]) -> i32 { values.iter().sum() }
この純粋関数は、入力と出力の関係が明確で、副作用がないため、テストが容易です。Rustの場合、所有権システムにより副作用の範囲が型レベルで明確になるため、さらにテストしやすくなります。一方、状態を持つコードは、その状態によって振る舞いが変わるため、テストが複雑になりがちです。
内部監査の重要性
著者は、完全にステートレスにできない場合の対策として、内部監査(internal auditing)の重要性を指摘しています。これは、コード内部で自己チェックを行うメカニズムを実装することで、状態の一貫性を保つ方法です。
例えば、Rustでは以下のように実装できます。
struct Character { // フィールド省略 } impl Character { fn audit(&self) -> Result<(), &'static str> { // 内部状態の一貫性をチェック if self.is_inconsistent() { return Err("Character state is inconsistent"); } Ok(()) } fn is_inconsistent(&self) -> bool { // 一貫性チェックのロジック false } }
Rustではpanic!よりもResult型でエラーを表現することが推奨されます。この内部監査を適切に配置することで、状態の不整合を早期に発見し、デバッグを容易にできます。
呼び出し側を信頼しない
著者は、「呼び出し側を信頼しない」という重要な原則を提示しています。これは、APIを設計する際に、不正な引数や不適切な使用方法を想定し、それらを適切に処理することの重要性を示しています。
例えば、Rustでは以下のように実装できます。
#[derive(Clone, Copy)] struct ObjectId { index: usize, generation: u32, } struct Simulator { index_generations: Vec<u32>, // その他のフィールド } impl Simulator { fn is_object_id_valid(&self, id: ObjectId) -> bool { id.index < self.index_generations.len() && self.index_generations[id.index] == id.generation } fn get_object_state(&self, id: ObjectId) -> Result<ObjectState, &'static str> { if !self.is_object_id_valid(id) { return Err("invalid object ID"); } // 以下、正常な処理 Ok(ObjectState::default()) } }
Rustではusizeが配列インデックスに使われるため、負数チェックが不要になります。また、Result型による明示的なエラーハンドリングにより、呼び出し側でのエラー処理が強制されます。このチェックを実装することで、APIの誤用を早期に検出し、デバッグを容易にできます。
まとめ
著者は、バグの「伝染性」という概念を通じて、早期発見と対処の重要性を強調しています。自動テスト、ステートレスなコード設計、内部監査、そして堅牢なAPIデザイン。これらを組み合わせることで、バグの影響を最小限に抑えられると主張しています。
これらの原則は、実際の開発現場でも有効です。特に、マイクロサービスアーキテクチャやサーバーレスコンピューティングが主流となっている現代のソフトウェア開発では、ステートレスなコード設計の重要性が増しています。また、CI/CDパイプラインの普及により、継続的な自動テストの実施が容易になっています。
しかし、著者も認めているように、これらの原則をすべての状況で完全に適用することは難しい場合もあります。例えば、レガシーシステムの保守や、リアルタイム性が要求される組み込みシステムの開発など、制約の多い環境では、これらの原則の適用に工夫が必要になるでしょう。
結論として、この章で提示されている原則は、バグの早期発見と対処を通じて、ソフトウェアの品質と保守性を高めるための重要な指針となります。これらの原則を理解し、プロジェクトの特性に応じて適切に適用することが、開発者には求められるのです。
Rule 3. A Good Name Is the Best Documentation
第3章「A Good Name Is the Best Documentation」は、プログラミングにおける命名の重要性を深く掘り下げています。著者は、適切な命名がコードの理解しやすさと保守性に大きな影響を与えることを強調し、良い命名がいかに効果的なドキュメンテーションになり得るかを説明しています。
この章では、命名の原則から具体的なプラクティス、そして命名規則の一貫性の重要性まで、幅広いトピックがカバーされています。著者の経験に基づく洞察は、日々のコーディングから大規模プロジェクトの設計まで適用できる実践的なアドバイスです。
言葉の形と意味の関連性については例えば、「ゴロゴロ」という言葉が雷の音を模倣しているように、言葉の音や形が、その意味を直接的に表現している場合があります。
この概念は、プログラミングの命名にも応用できる可能性があります。機能や役割を直感的に表現する変数名やメソッド名を選ぶことで、コードの理解しやすさを向上させることができるかもしれません。ただし、プログラムの複雑化に伴い、単純な音や形の類似性だけでは不十分になる場合もあるため、コンテキストや他の命名規則との整合性も考慮する必要があります。
命名の重要性
著者は、シェイクスピアの「ロミオとジュリエット」を引用しながら、名前の持つ力について語り始めます。「バラはどんな名前で呼んでも、同じように甘い香りがする」というジュリエットの台詞を、プログラミングの文脈で解釈し直しています。
著者の主張は明確です。コードにおいて、名前は単なるラベル以上の意味を持つのです。適切な名前は、そのコードの目的や機能を即座に伝える強力なツールとなります。これは、コードを書く時間よりも読む時間の方が圧倒的に長いという現実を考えると、重要な指摘です。
実際の開発現場でも、この原則の重要性は日々実感されます。例えば、数ヶ月前に書いたコードを見直す時、適切な名前付けがされていれば、コードの意図を素早く理解できます。逆に、意味の曖昧な変数名やメソッド名に遭遇すると、コードの解読に余計な時間を取られてしまいます。
最小限のキーストロークを避ける
著者は、変数名や関数名を短くすることで、タイピング時間を節約しようとする傾向について警告しています。これは特に、経験の浅い開発者や古い時代のプログラミング習慣を持つ開発者に見られる傾向です。
例として、複素数の多項式を評価する関数のコードが示されています。最初の例では、変数名が極端に短く、コードの意図を理解するのが困難です。一方、適切な名前を使用した第二の例では、コードの意図が明確になり、理解しやすくなっています。
// 悪い例 fn cp(n: usize, rr: &[f64], ii: &[f64], xr: f64, xi: f64) -> (f64, f64) { // ... (省略) (0.0, 0.0) } // 良い例 fn evaluate_complex_polynomial( degree: usize, real_coeffs: &[f64], imag_coeffs: &[f64], real_x: f64, imag_x: f64, ) -> (f64, f64) { // ... (省略) (0.0, 0.0) }
Rustではsnake_caseが標準的な命名規則であり、これに従うことでコードの一貫性が保たれます。この例は、適切な命名がいかにコードの可読性を向上させるかを明確に示しています。長い名前を使用することで、コードを書く時間は若干増えるかもしれませんが、それ以上に読む時間と理解する時間が大幅に短縮されます。
命名規則の一貫性
著者は、プロジェクト内で一貫した命名規則を使用することの重要性を強調しています。異なる命名規則が混在すると、コードの理解が困難になり、認知負荷が増大します。
例えば、自作のコンテナクラスと標準ライブラリのコンテナクラスを混在して使用する場合、命名規則の違いによって混乱が生じる可能性があります。著者は、可能な限り一貫した命名規則を採用し、外部ライブラリの使用を最小限に抑えることを提案しています。
実際の開発現場では、チーム全体で一貫した命名規則を採用することが重要です。例えば、Rustでは以下のような命名規則が標準です。
// 良い例(Rust標準に従う) struct User { id: i32, first_name: String, last_name: String, } impl User { fn full_name(&self) -> String { format!("{} {}", self.first_name, self.last_name) } } // 悪い例(一貫性がない) struct Customer { ID: i32, // PascalCaseはフィールドに使わない firstName: String, // camelCaseはRustでは非標準 last_name: String, } impl Customer { fn GetFullName(&self) -> String { // PascalCaseはメソッドに使わない format!("{} {}", self.firstName, self.last_name) } }
Rustでは構造体名はPascalCase、フィールドとメソッドはsnake_caseが標準です。rustfmtとclippyがこれらの規則違反を検出してくれるため、チーム全体で一貫性を保ちやすくなっています。
機械的な命名規則の利点
著者は、可能な限り機械的な命名規則を採用することを推奨しています。これにより、チームメンバー全員が自然に同じ名前を選択するようになり、コードベース全体の一貫性が向上します。
著者の所属するSucker Punchでは、Microsoftのハンガリアン記法の変種を使用しているそうです。例えば、iFactionは配列内のインデックスを、vpCharacterはキャラクターへのポインタのベクトルを表します。
これは興味深いアプローチですが、現代のプログラミング言語やIDE環境では必ずしも必要ないかもしれません。例えば、Rustでは型システムが強力で、rust-analyzerのサポートも充実しています。そのため、以下のような命名規則でも十分に明確であり、かつ読みやすいコードを書くことができます。
fn process_users(users: &[User], active_only: bool) -> Vec<User> { users .iter() .filter(|user| !active_only || user.is_active) .cloned() .collect() }
この例では、変数の型や用途が名前自体から明確に分かります。usersは複数のユーザーを表すスライス、active_onlyはブール値のフラグです。Rustのイテレータを使うことで、処理の意図がより宣言的に表現されています。
まとめ
著者は、良い命名が最良のドキュメンテーションであるという主張を、複数の角度から論じています。適切な命名は、コードの意図を即座に伝え、保守性を高め、チーム全体の生産性を向上させます。
一方で、命名規則に関しては、プロジェクトやチームの状況に応じて柔軟に対応することも重要です。例えば、レガシーコードベースを扱う場合や、異なる背景を持つ開発者が協働する場合など、状況に応じた判断が求められます。
私の経験上、最も重要なのはチーム内での合意形成です。どのような命名規則を採用するにせよ、チーム全体がその規則を理解し、一貫して適用することが、コードの可読性と保守性を高める鍵となります。
また、命名規則は時代とともに進化することも忘れてはいけません。例えば、かつては変数名の長さに制限があったため短い名前が好まれましたが、現代の開発環境ではそのような制限はほとんどありません。そのため、より説明的で長い名前を使用することが可能になっています。
結論として、良い命名はコードの品質を大きく左右する重要な要素です。it's not just about writing code, it's about writing code that tells a story. その物語を明確に伝えるために、私たちは日々、より良い命名を追求し続ける必要があるのです。
Rule 4. Generalization Takes Three Examples
第4章「Generalization Takes Three Examples」は、ソフトウェア開発における一般化(generalization)の適切なタイミングと方法について深く掘り下げています。著者は、コードの一般化が重要でありながらも、早すぎる一般化が引き起こす問題について警鐘を鳴らしています。この章を通じて、プログラマーが日々直面する「特定の問題を解決するコードを書くべきか、それとも汎用的な解決策を目指すべきか」というジレンマに対する洞察を提供しています。
この章の内容は、認知心理学の知見とも関連しており、即座に解決策を求める直感的な思考は特定の問題に対する迅速な解決をもたらす一方で過度の一般化につながる危険性がある一方、より慎重で分析的な思考は複数の事例を比較検討し適切なレベルの一般化を導く可能性が高くなるため、著者が提案する「3つの例則」は、より適切な一般化を実現するための実践的なアプローチとして、ソフトウェア開発における意思決定プロセスを理解し改善するための新たな洞察を提供してくれるでしょう。
一般化の誘惑
著者は、プログラマーが一般的な解決策を好む傾向について語ることから始めます。例えば、赤い看板を見つける関数を書く代わりに、色を引数として受け取る汎用的な関数を書くことを選ぶプログラマーが多いと指摘しています。
// 特定の解決策 fn find_red_sign(signs: &[Sign]) -> Option<&Sign> { signs.iter().find(|sign| sign.color() == Color::Red) } // 一般的な解決策 fn find_sign_by_color(signs: &[Sign], color: Color) -> Option<&Sign> { signs.iter().find(|sign| sign.color() == color) }
RustではOption型により「見つからない」という状態を型レベルで表現できます。この例は、多くのプログラマーにとって馴染み深いものでしょう。私自身、これまでの経験で何度も同様の選択を迫られてきました。一般的な解決策を選ぶ理由として、将来的な拡張性や再利用性を挙げる人が多いですが、著者はここで重要な問いを投げかけています。本当にその一般化は必要なのか?
YAGNIの原則
著者は、XP(エクストリーム・プログラミング)の原則の一つである「YAGNI」(You Ain't Gonna Need It:それは必要にならないよ)を引用しています。この原則は、実際に必要になるまで機能を追加しないことを提唱しています。こういう原則は『プリンシプル オブ プログラミング3年目までに身につけたい一生役立つ101の原理原則』を読めば一通り読めるのでおすすめです。
例えば、看板検索の例をさらに一般化して、色だけでなく、場所やテキストなども検索できるようにした SignQuery 構造体を考えてみます。
struct SignQuery { colors: Vec<Color>, location: Location, max_distance: f64, text_pattern: String, } fn find_signs(query: &SignQuery, signs: &[Sign]) -> Vec<Sign> { // 実装省略 Vec::new() }
この SignQuery は柔軟で強力に見えますが、著者はこのアプローチに警鐘を鳴らします。なぜなら、この一般化された構造は、実際には使用されない機能を含んでいる可能性が高いからです。さらに重要なことに、この一般化された構造は、将来の要件変更に対して柔軟に対応できないかもしれません。
3つの例則
著者は、一般化を行う前に少なくとも3つの具体的な使用例を見るべきだと主張します。これは、良い視点だと思いました。1つや2つの例では、パターンを正確に把握するには不十分で、誤った一般化を導く可能性があります。3つの例を見ることで、より正確なパターンの把握と、より控えめで適切な一般化が可能になるという考えは説得力があります。
実際の開発現場では、この「3つの例則」を厳密に適用するのは難しいかもしれません。しかし、この原則を意識することで、早すぎる一般化を避け、より適切なタイミングで一般化を行うことができるでしょう。
過度な一般化の危険性
著者は、過度に一般化されたコードがもたらす問題について詳しく説明しています。特に印象的だったのは、一般化されたソリューションが「粘着性」を持つという指摘です。つまり、一度一般化された解決策を採用すると、それ以外の方法を考えるのが難しくなるということです。
例えば、findSigns 関数を使って赤い看板を見つけた後、他の種類の看板を見つける必要が出てきたとき、多くのプログラマーは自然と findSigns 関数を拡張しようとするでしょう。しかし、これが必ずしも最適な解決策とは限りません。
// 過度に一般化された関数 fn find_signs(query: &ComplexQuery, signs: &[Sign]) -> Vec<Sign> { // 複雑な実装 Vec::new() } // 単純で直接的な解決策 fn find_blue_signs_on_main_street(signs: &[Sign]) -> Vec<Sign> { signs .iter() .filter(|sign| sign.color() == Color::Blue && is_on_main_street(sign.location())) .cloned() .collect() }
この例では、find_signs を使用するよりも、直接的な解決策の方がシンプルで理解しやすいことがわかります。Rustのイテレータチェーンにより、処理の意図が明確に表現されています。著者の主張は、一般化されたソリューションが常に最適とは限らず、時には直接的なアプローチの方が優れている場合があるということです。
まとめ:このルールが死ぬとき
「3つの例を待て」という原則は正しい。しかし、現場ではこのルールが政治的・組織的な理由で死ぬことがある。
一般化ルールが死んだ現場:「重複コードは悪」という空気 あるプロジェクトで、認証ロジックが2つのサービスで似たような実装になっていた。私は3つ目のユースケースを待つつもりだった。しかし、コードレビューで「DRY違反では?」と指摘が入り、共通ライブラリ化を迫られた。政治的に抵抗できず、2つの例だけで共通化した。
結果、3つ目のサービス(外部向けAPI)では、その共通ライブラリが使えなかった。認証フローが根本的に異なったからだ。共通ライブラリには「外部向けモード」というフラグが追加され、コードは複雑化した。3つ目を待っていれば、「これは共通化すべきでない」と判断できたはずだ。
逆に、3つ待てなかった正当なケース 一方で、3つ待てない状況もある。セキュリティ脆弱性の修正で、2つのサービスに同じパッチを当てる必要があった場合、「3つ目を待とう」とは言えない。即座に共通化し、両方に適用すべきだ。
このルールの適用限界 - コードレビュー文化が「重複=悪」に偏っている組織では、政治的に適用困難 - セキュリティやコンプライアンス要件では、3つ待つ余裕がないことがある - チームの入れ替わりが激しい環境では、「3つ目の例を待つ」という文脈が失われやすい
「3つの例」は科学的な数字ではない。重要なのは「パターンの確信度」だ。2つでも確信があれば共通化してよいし、4つあっても疑わしければ待つべきだ。
Rule 5. The First Lesson of Optimization Is Don't Optimize
第5章「The First Lesson of Optimization Is Don't Optimize」は、ソフトウェア開発における最も誤解されやすい、そして最も議論を呼ぶトピックの一つである最適化について深く掘り下げています。著者は、最適化に対する一般的な考え方に挑戦し、実践的かつ効果的なアプローチを提案しています。
この章では、最適化の本質、その落とし穴、そして効果的な最適化の方法について詳細に解説されています。著者の経験に基づく洞察は、日々のコーディング作業から大規模プロジェクトの設計まで、様々な場面で適用できる実践的なアドバイスとなっています。
最適化の誘惑
著者は、最適化が多くのプログラマーにとって魅力的なタスクであることを認めています。最適化は、その成功を明確に測定できるという点で、他のプログラミングタスクとは異なります。しかし、著者はこの誘惑に警鐘を鳴らします。
ここで著者が引用しているドナルド・クヌースの言葉は、多くのプログラマーにとってお馴染みのものです。
小さな効率性については97%の時間を忘れるべきである:早すぎる最適化は諸悪の根源である。
この言葉は、最適化に対する慎重なアプローチの必要性を強調しています。著者は、この原則が現代のソフトウェア開発においても依然として重要であることを主張しています。
最適化の第一の教訓
著者が強調する最適化の第一の教訓は、「最適化するな」というものです。これは一見矛盾しているように見えますが、著者の意図は明確です。最初から最適化を意識してコードを書くのではなく、まずはシンプルで明確なコードを書くべきだというのです。
この原則を実践するための具体例として、著者は重み付きランダム選択の関数を挙げています。最初の実装は以下のようなものです。
use rand::Rng; fn choose_random_value<T: Clone>(weights: &[u32], values: &[T]) -> T { let total_weight: u32 = weights.iter().sum(); let mut select_weight = rand::thread_rng().gen_range(0..total_weight); for (i, &weight) in weights.iter().enumerate() { if select_weight < weight { return values[i].clone(); } select_weight -= weight; } unreachable!("weights must not be empty") }
Rustではジェネリクスにより型安全な汎用関数を書けます。また、unreachable!マクロは到達不能なコードを明示し、コンパイラの最適化を助けます。この実装は単純明快で、理解しやすいものです。著者は、この段階で最適化を考えるのではなく、まずはこのシンプルな実装で十分だと主張します。
最適化の第二の教訓
著者が提唱する最適化の第二の教訓は、「シンプルなコードは簡単に最適化できる」というものです。著者は、未最適化のコードであれば、大きな労力をかけずに5倍から10倍の速度向上を達成できると主張します。
この主張を実証するため、著者は先ほどのchooseRandomValue関数の最適化に挑戦します。著者が提案する最適化のプロセスは以下の5ステップです。
- プロセッサ時間を測定し、属性付けする
- バグでないことを確認する
- データを測定する
- 計画とプロトタイプを作成する
- 最適化し、繰り返す
このプロセスに従って最適化を行った結果、著者は元の実装の約12倍の速度を達成しました。これは、著者の「5倍から10倍の速度向上」という主張を裏付けるものです。
過度な最適化の危険性
著者は、一度目標の速度向上を達成したら、それ以上の最適化は避けるべきだと警告しています。これは、過度な最適化が複雑性を増し、コードの可読性や保守性を損なう可能性があるためです。
著者自身、さらなる最適化のアイデアを持っていることを認めていますが、それらを追求する誘惑に抗うことの重要性を強調しています。代わりに、それらのアイデアをコメントとして残し、将来必要になった時のために保存しておくことを提案しています。
まとめ:このルールが死ぬとき
「最適化するな」は、私がSREとして最も頻繁に「殺す」ルールだ。
最適化ルールが最初から死んでいた現場:リアルタイム要件 Kubernetesのカスタムコントローラを開発した際、最初から最適化を意識せざるを得なかった。Reconciliation loopが100ms以内に完了しないと、クラスタ全体の安定性に影響する。「まずシンプルに書いて、後で最適化」では間に合わない。最初からホットパスを意識し、メモリアロケーションを最小化する設計が必要だった。
最適化を「しなかった」ことで死んだ現場 逆のパターンもある。ある内部ツールで「どうせ1日1回しか動かないから」と最適化を完全に無視した。処理時間30分。問題なく動いていた。しかし、そのツールが「1時間ごとに動かしたい」という要件変更を受けた時、最適化が必要になった。シンプルに書いていたはずのコードは、実は暗黙の前提(「1日1回」)に依存しており、最適化は想像以上に困難だった。
このルールの適用限界 - レイテンシ要件が明確に定義されているシステムでは、最初から考慮が必要 - バッチ処理でも、将来の要件変更を見越したアーキテクチャ選択は必要 - 「後で最適化できる」は、アーキテクチャレベルでは成り立たないことが多い
クヌースの言葉は「97%の時間」と言っている。残り3%を見極めるのがエンジニアの仕事だ。「最適化するな」ではなく「この最適化は97%に属するか、3%に属するか」を問うべきだ。
Rule 6. Code Reviews Are Good for Three Reasons
第6章「コードレビューが良い3つの理由」は、ソフトウェア開発プロセスにおけるコードレビューの重要性と、その多面的な利点について深く掘り下げています。著者は、自身の30年以上にわたるプログラミング経験を基に、コードレビューの進化と現代のソフトウェア開発における不可欠な役割を論じています。
この章では、コードレビューが単なるバグ発見のツールではなく、知識共有、コード品質向上、そしてチーム全体の生産性向上に寄与する重要な実践であることを示しています。著者の洞察は、現代のアジャイル開発やDevOpsの文脈においても関連性が高く、多くの開発チームにとって有益な示唆を提供しています。
コードレビューの効果を最大化するには、適切なフィードバック方法が重要です。建設的なフィードバックの与え方、受け手の心理を考慮したコミュニケーション、ポジティブな点も含めたバランスのとれたコメントが求められます。コードレビューを単なる技術的な確認作業ではなく、チームの成長を促進する機会として活用することで、コミュニケーションが改善され、開発プロセス全体の効率と品質が向上します。
コードレビューの進化
著者は、コードレビューが過去30年間でどのように進化してきたかを振り返ることから始めます。かつてはほとんど行われていなかったコードレビューが、現在では多くの開発チームで標準的な実践となっていることを指摘しています。
この変化は、ソフトウェア開発の複雑化と、チーム開発の重要性の増大を反映しているように思います。個人的な経験を踏まえると、10年前と比べても、コードレビューの重要性に対する認識は格段に高まっていると感じます。特に、オープンソースプロジェクトの台頭や、GitHubなどのプラットフォームの普及により、コードレビューの文化はさらに広がっていると言えるでしょう。
近年、生成AIを活用したコードレビューツールも注目を集めています。例えばPR-agentやGitHub Copilot pull requestは、AIがプルリクエストを分析し、フィードバックを提供します。このようなツールは、人間のレビューアーを補完し、効率的なコード品質管理を可能にします。
ただし、AIによるレビューには限界もあります。コンテキストの理解や創造的な問題解決など、人間のレビューアーの強みは依然として重要です。そのため、AIツールと人間のレビューを組み合わせたハイブリッドアプローチが、今後のベストプラクティスとなる可能性があります。
コードレビューの3つの利点
著者は、コードレビューには主に3つの利点があると主張しています。
- バグの発見
- 知識の共有
- コード品質の向上
これらの利点について、著者の見解を踏まえつつ、現代のソフトウェア開発の文脈で考察してみます。
1. バグの発見
著者は、バグ発見がコードレビューの最も明白な利点であるものの、実際にはそれほど効果的ではないと指摘しています。確かに、私の経験でも、コードレビューで見つかるバグは全体の一部に過ぎません。
しかし、ここで重要なのは、コードレビューにおけるバグ発見のプロセスです。著者が指摘するように、多くの場合、バグはレビューを受ける側が説明する過程で自ら気づくことが多いのです。これは、ラバーダッキング手法の一種と見なすこともできます。
2. 知識の共有
著者は、コードレビューが知識共有の優れた方法であると強調しています。これは、現代の開発環境において特に重要な点です。
技術の進化が速く、プロジェクトの規模が大きくなる中で、チーム全体の知識レベルを均一に保つことは難しくなっています。コードレビューは、この課題に対する効果的な解決策の一つです。
著者が提案する「シニア」と「ジュニア」の組み合わせによるレビューは、特に有効だと考えます。ただし、ここでの「シニア」「ジュニア」は、必ずしも経験年数ではなく、特定の領域やプロジェクトに対する知識の深さを指すと解釈するべきでしょう。
3. コード品質の向上
著者は、コードレビューの最も重要な利点として、「誰かが見るということを知っていると、みんなより良いコードを書く」という点を挙げています。この指摘は的を射ていると思います。
人間の心理として、他人に見られることを意識すると、自然とパフォーマンスが向上します。これは、ソフトウェア開発においても例外ではありません。コードレビューの存在自体が、コード品質を向上させる強力な動機付けとなるのです。
コードレビューの実践
著者は、自社でのコードレビューの実践について詳しく説明しています。リアルタイムで、インフォーマルに、対話形式で行われるこのアプローチは、多くの利点があります。
特に印象的なのは、レビューをダイアログとして捉える視点です。一方的なチェックではなく、相互の対話を通じて理解を深めていくこのアプローチは、知識共有と問題発見の両面で効果的です。
一方で、この方法はリモートワークが増加している現代の開発環境では、そのまま適用するのが難しい場合もあります。しかし、ビデオ会議ツールやペアプログラミングツールを活用することで、類似の効果を得ることは可能です。
まとめ
著者の「コードレビューには3つの良い理由がある」という主張は、説得力があります。バグの発見、知識の共有、コード品質の向上という3つの側面は、いずれも現代のソフトウェア開発において重要な要素です。
しかし、これらの利点を最大限に引き出すためには、著者が強調するように、コードレビューを単なる形式的なプロセスではなく、チームのコミュニケーションと学習の機会として捉えることが重要です。
個人的な経験を踏まえると、コードレビューの質は、チームの文化と深く関連していると感じます。オープンで建設的なフィードバックを歓迎する文化、継続的な学習を重視する文化を育てることが、効果的なコードレビューの前提条件となるでしょう。
また、著者が指摘する「禁止されたコードレビュー」(ジュニア同士のレビュー)については、少し異なる見解を持ちます。確かに、知識の誤った伝播というリスクはありますが、ジュニア同士であっても、互いの視点から学ぶことはあると考えます。ただし、これには適切な監視とフォローアップが必要です。
最後に、コードレビューは決して完璧なプロセスではありません。著者も認めているように、全てのバグを見つけることはできません。しかし、それでもコードレビューは、ソフトウェアの品質向上とチームの成長に大きく貢献する貴重な実践です。
Rule 7. Eliminate Failure Cases
第7章「Eliminate Failure Cases」は、ソフトウェア開発における失敗ケースの排除という重要なトピックを深く掘り下げています。この章を通じて、著者は失敗ケースの排除がプログラムの堅牢性と信頼性を高める上で不可欠であることを強調し、その実践的なアプローチを提示しています。
失敗ケースとは何か
著者はまず、失敗ケースの定義から始めています。失敗ケースとは、プログラムが想定外の動作をする可能性のある状況のことです。例えば、ファイルの読み込みに失敗したり、ネットワーク接続が切断されたりする場合などが挙げられます。著者は、これらの失敗ケースを完全に排除することは不可能だが、多くの場合で回避または最小化できると主張しています。
この考え方は、エラーハンドリングに対する従来のアプローチとは異なります。多くの開発者は、エラーが発生した後にそれをどう処理するかに焦点を当てがちですが、著者はエラーが発生する可能性自体を減らすことに重点を置いています。これは、防御的プログラミングの一歩先を行く考え方だと言えるでしょう。
失敗ケースの排除方法
著者は、失敗ケースを排除するための具体的な方法をいくつか提示しています。
型安全性の活用:強い型付けを持つ言語を使用することで、多くの失敗ケースを compile time に検出できます。
nullの回避:null参照は多くのバグの源となるため、できる限り避けるべきです。Optionalパターンなどの代替手段を使用することを推奨しています。
不変性の活用:データを不変に保つことで、予期せぬ状態変更による失敗を防ぐことができます。
契約による設計:事前条件、事後条件、不変条件を明確に定義することで、関数やメソッドの正しい使用を強制できます。
これらの方法は、単に失敗ケースを処理するのではなく、失敗ケースが発生する可能性自体を減らすことを目指しています。
コンパイラの助けを借りる
著者は、失敗ケースの排除においてコンパイラの重要性を強調しています。静的型付け言語のコンパイラは、多くの潜在的な問題を事前に検出できます。例えば、未使用の変数や、型の不一致などを検出し、コンパイル時にエラーを報告します。
これは、動的型付け言語と比較して大きな利点です。動的型付け言語では、これらの問題が実行時まで検出されない可能性があります。著者は、可能な限り多くのチェックをコンパイル時に行うことで、実行時エラーのリスクを大幅に減らせると主張しています。
設計による失敗ケースの排除
著者は、適切な設計によって多くの失敗ケースを排除できると主張しています。例えば、状態機械(state machine)を使用することで、無効な状態遷移を防ぐことができます。また、ファクトリーメソッドパターンを使用することで、オブジェクトの不正な初期化を防ぐこともできます。
これらの設計パターンを適切に使用することで、コードの構造自体が失敗ケースを排除する役割を果たすことができます。つまり、プログラムの設計段階から失敗ケースの排除を意識することの重要性を著者は強調しています。
失敗ケース排除の限界
著者は、全ての失敗ケースを排除することは不可能であることも認めています。例えば、ハードウェアの故障やネットワークの遮断など、プログラムの制御外の要因によるエラーは避けられません。しかし、著者はこれらの避けられない失敗ケースに対しても、その影響を最小限に抑える設計が可能だと主張しています。
例えば、トランザクションの使用や、べき等性のある操作の設計などが、これらの戦略として挙げられています。これらの方法を使用することで、予期せぬエラーが発生しても、システムを一貫性のある状態に保つことができます。
まとめ
著者は、失敗ケースの排除が単なるエラーハンドリングの改善以上の意味を持つと主張しています。それは、プログラムの設計と実装の全体的な質を向上させる取り組みなのです。失敗ケースを排除することで、コードはより堅牢になり、バグの発生率が減少し、結果として保守性が向上します。
この章から得られる重要な教訓は、エラーを処理する方法を考えるだけでなく、エラーが発生する可能性自体を減らすことに注力すべきだということです。これは、プログラミングの哲学的なアプローチの変更を意味します。
私自身、この原則を実践することで、コードの品質が大幅に向上した経験があります。例えば、nullの使用を避け、Optionalパターンを採用することで、null pointer exceptionの発生率を大幅に減らすことができました。また、型安全性を重視することで、多くのバグを compile time に検出し、デバッグにかかる時間を削減することができました。
ただし、著者の主張にも若干の批判的な視点を加えるならば、失敗ケースの完全な排除を目指すことで、かえってコードが複雑になり、可読性が低下する可能性もあります。そのため、失敗ケースの排除と、コードの簡潔さのバランスを取ることが重要です。
最後に、この章の教訓は、単に個々の開発者のコーディング習慣を改善するだけでなく、チーム全体の開発プロセスや設計方針にも適用できます。例えば、コードレビューの基準に「失敗ケースの排除」を含めたり、アーキテクチャ設計の段階で潜在的な失敗ケースを特定し、それらを排除する戦略を立てたりすることができます。
この原則を実践することで、より信頼性の高い、堅牢なソフトウェアを開発することができるでしょう。それは、単にバグの少ないコードを書くということだけでなく、予測可能で、管理しやすい、高品質なソフトウェアを作り出すことを意味します。これは、長期的な視点で見たときに、開発効率の向上とメンテナンスコストの削減につながる重要な投資だと言えるでしょう。
Rule 8. Code That Isn't Running Doesn't Work
第8章「Code That Isn't Running Doesn't Work」は、ソフトウェア開発における重要だが見落とされがちな問題、すなわち使用されていないコード(デッドコード)の危険性について深く掘り下げています。著者は、一見無害に見えるデッドコードが、実際にはプロジェクトの健全性と保守性に大きな影響を与える可能性があることを、具体的な例を通じて説明しています。この章を通じて、コードベースの進化と、それに伴う予期せぬ問題の発生メカニズムについて、実践的な洞察が提供されています。
ソフトウェア開発において、「疲れないコード」を作ることも重要です。疲れないコードとは、読みやすく、理解しやすく、そして保守が容易なコードを指します。このようなコードは、長期的なプロジェクトの健全性を維持し、開発者の生産性を向上させる上で極めて重要です。疲れないコードを書くことで、デッドコードの発生を防ぎ、コードベース全体の品質を高めることができるのです。
デッドコードの定義と危険性
著者は、デッドコードを「かつては使用されていたが、現在は呼び出されていないコード」と定義しています。これは一見、単なる無駄なコードに過ぎないように思えるかもしれません。しかし、著者はデッドコードが単なる無駄以上の問題を引き起こす可能性があることを強調しています。
デッドコードの危険性は、それが「動作しているかどうか分からない」という点にあります。使用されていないコードは、周囲のコードの変更に応じて更新されることがありません。そのため、いつの間にか古くなり、バグを含む可能性が高くなります。さらに悪いことに、そのバグは誰にも気付かれません。なぜなら、そのコードは実行されていないからです。
この状況を、著者は「シュレディンガーの猫」になぞらえています。デッドコードは、箱の中の猫のように、観察されるまでその状態(正常か異常か)が分かりません。そして、いざそのコードが再び使用されたとき、予期せぬバグが顕在化する可能性があるのです。
コードの進化と予期せぬ問題
著者は、コードベースの進化過程を川の流れに例えています。川の流れが変わるように、コードの使用パターンも時間とともに変化します。その過程で、かつては重要だった機能が使われなくなることがあります。これがデッドコードの発生源となります。
著者は、この進化の過程を4つのステップに分けて説明しています。各ステップで、コードベースがどのように変化し、それに伴ってどのような問題が潜在的に発生するかを詳細に解説しています。特に印象的だったのは、一見無関係に見える変更が、思わぬところでバグを引き起こす可能性があるという指摘です。
例えば、あるメソッドが使われなくなった後、そのメソッドに関連する新機能が追加されたとします。このとき、そのメソッドは新機能に対応するように更新されないかもしれません。そして後日、誰かがそのメソッドを再び使用しようとしたとき、予期せぬバグが発生する可能性があるのです。
この例は、デッドコードが単なる無駄以上の問題を引き起こす可能性を明確に示しています。デッドコードは、時間の経過とともに「時限爆弾」となる可能性があるのです。
デッドコードの検出と対策
著者は、デッドコードの問題に対する一般的な対策として、ユニットテストの重要性を認めつつも、その限界についても言及しています。確かに、すべてのコードにユニットテストを書くことで、使用されていないコードも定期的にテストされることになります。しかし、著者はこのアプローチにも問題があると指摘しています。
テストの維持コスト:使用されていないコードのテストを維持することは、それ自体が無駄なリソースの消費となる可能性があります。
テストの不完全性:ユニットテストは、実際の使用環境でのすべての状況を網羅することは困難です。特に、コードベース全体の変更に伴う影響を完全にテストすることは難しいでしょう。
誤った安心感:テストが通っているからといって、そのコードが実際の使用環境で正しく動作する保証にはなりません。
これらの理由から、著者はデッドコードに対する最も効果的な対策は、それを積極的に削除することだと主張しています。
デッドコード削除の実践
著者の主張は、一見過激に感じるかもしれません。使えそうなコードを削除するのは、もったいないと感じる開発者も多いでしょう。しかし、著者はデッドコードを削除することのメリットを以下のように説明しています。
コードベースの簡素化:使用されていないコードを削除することで、コードベース全体が小さくなり、理解しやすくなります。
保守性の向上:デッドコードを削除することで、将来的なバグの可能性を減らすことができます。
パフォーマンスの向上:使用されていないコードを削除することで、コンパイル時間やビルド時間を短縮できる可能性があります。
誤用の防止:存在しないコードは誤って使用されることがありません。
著者は、デッドコードを発見したら、それを喜びとともに削除するべきだと主張しています。これは、単にコードを削除するということではなく、プロジェクトの健全性を向上させる積極的な行為なのです。
まとめ:このルールが死ぬとき
「動いていないコードは動かない」—これは真実だ。私自身、デッドコードが3年後に「時限爆弾」として爆発した経験がある。しかし、このルールを適用しようとして「死んだ」経験もある。
デッドコード削除ルールが死んだ現場:「万が一のために」 あるレガシーシステムで、明らかに使われていない認証モジュールを発見した。git blameを見ると2年間触られていない。削除を提案したところ、「万が一のロールバック用に残しておきたい」と却下された。上司の判断だ。政治的に抵抗できなかった。
結果、3年後にそのモジュールがセキュリティスキャンに引っかかった。使われていないのに脆弱性として報告され、「修正」が必要になった。使われていないコードの「修正」ほど無意味な作業はない。ルールの正しさは証明されたが、適用には別の能力—政治力—が必要だった。
デッドコードを「削除できない」正当なケース 一方で、削除すべきでないケースもある。 - フィーチャーフラグで無効化されているが、A/Bテストで復活予定のコード - 廃止予定だが、移行期間中は互換性のために残すAPI - ライブラリとして公開しており、外部利用者がいる可能性があるコード
このルールの適用限界 - 「削除」の判断には、コードだけでなくビジネスコンテキストの理解が必要 - レガシーシステムでは、「使われていない」ことの証明が困難な場合がある - チームの合意なしに削除すると、信頼関係が損なわれる
デッドコードの削除は技術的判断ではなく、組織的判断だ。コードを消すことより、「なぜこのコードが生まれ、なぜ放置されたか」を理解することの方が重要な場合がある。
ちなみに、「使われていないコードを修正する」という行為は禅問答に似ている。「片手の音を聞け」と言われて困惑するように、私も困惑した。使われていないなら、修正後も使われていない。では何を修正したのか。セキュリティスキャンツールの心だろうか。
Rule 9. Write Collapsible Code
第9章「Write Collapsible Code」は、コードの可読性と理解のしやすさに焦点を当てた重要な原則を提示しています。著者は、人間の認知能力、特に短期記憶の限界を考慮に入れたコード設計の重要性を強調しています。この章を通じて、ソフトウェア開発者が直面する「コードの複雑さをいかに管理するか」という永遠の課題に対する実践的なアプローチが示されています。
短期記憶の限界とコードの理解
著者は、人間の短期記憶が平均して7±2個の項目しか保持できないという心理学的な知見を基に議論を展開しています。これは、コードを読む際にも同様に適用され、一度に理解できる情報量に限界があることを意味します。
この観点から、著者は「コードの崩壊性(collapsibility)」という概念を提唱しています。これは、コードの各部分が容易に抽象化され、単一の概念として理解できるようになっている状態を指します。
抽象化の重要性と落とし穴
著者は、適切な抽象化が「崩壊性のあるコード」を書く上で重要だと主張しています。しかし、過度な抽象化は逆効果になる可能性があることも指摘しています。
チームの共通知識の活用
著者は、チーム内で広く理解されている概念や慣用句を活用することの重要性を強調しています。これらは既にチームメンバーの長期記憶に存在するため、新たな短期記憶の負担を生みません。
新しい抽象化の導入
著者は、新しい抽象化を導入する際の慎重さも強調しています。新しい抽象化は、それが広く使用され、チームの共通知識となるまでは、かえってコードの理解を難しくする可能性があります。
まとめ
著者の「Write Collapsible Code」という原則は、コードの可読性と保守性を高める上で重要です。この原則は、人間の認知能力の限界を考慮に入れたソフトウェア設計の重要性を強調しています。
コードの「崩壊性」を意識することで、開発者は自然と適切な抽象化レベルを選択し、チームの共通知識を活用したコードを書くようになります。これは、長期的にはコードベース全体の品質向上につながります。
ただし、「崩壊性」の追求が過度の単純化や不適切な抽象化につながらないよう注意が必要です。適切なバランスを見出すには、継続的な練習と経験が必要でしょう。
最後に、この原則は特定の言語や環境に限定されるものではありません。様々なプログラミングパラダイムや開発環境において、「崩壊性のあるコード」を書くという考え方は普遍的に適用できます。
Rule 10. Localize Complexity
第10章「Localize Complexity」は、ソフトウェア開発における複雑性の管理という重要なトピックを深く掘り下げています。著者は、プロジェクトの規模が大きくなるにつれて複雑性が増大し、それがコードの保守性や拡張性に大きな影響を与えることを指摘しています。この章を通じて、複雑性を完全に排除することは不可能だが、それを効果的に局所化することで管理可能にする方法が示されています。
複雑性の本質と影響
著者は冒頭で「Complexity is the enemy of scale」という強烈な一文を投げかけています。この言葉は、私の15年のエンジニア経験を通じて痛感してきたことでもあります。小規模なプロジェクトでは気にならなかった複雑性が、プロジェクトの成長とともに指数関数的に増大し、開発速度を著しく低下させる様子を何度も目の当たりにしてきました。
著者は、複雑性が増大すると、コードの全体像を把握することが困難になり、バグの修正や新機能の追加が予期せぬ副作用を引き起こすリスクが高まると指摘しています。これは、特に長期的なプロジェクトや大規模なシステムにおいて顕著な問題となります。
複雑性の局所化
著者は、複雑性を完全に排除することは不可能だが、それを効果的に「局所化」することで管理可能になると主張しています。これは重要な洞察です。
例えば、著者はsin関数やcos関数の実装を例に挙げています。これらの関数の内部実装は複雑ですが、外部から見たインターフェースはシンプルです。この「複雑性の隠蔽」こそが、優れた設計の本質だと言えるでしょう。
この原則は、モダンなソフトウェア開発手法とも密接に関連しています。例えば、マイクロサービスアーキテクチャは、複雑なシステムを比較的独立した小さなサービスに分割することで、全体の複雑性を管理可能にする手法です。各サービスの内部は複雑であっても、サービス間のインターフェースをシンプルに保つことで、システム全体の複雑性を抑制することができます。
複雑性の増大を防ぐ実践的アプローチ
著者は、複雑性の増大を防ぐための具体的なアプローチをいくつか提示しています。特に印象的だったのは、「同じロジックを複数の場所に実装しない」という原則です。
著者は、このアプローチの問題点を明確に指摘しています。新しい条件が追加されるたびに、全ての実装箇所を更新する必要が生じ、コードの保守性が急速に低下します。これは、私が「コピペプログラミング」と呼んでいる悪しき習慣そのものです。
代わりに著者が提案しているのは、状態の変更を検知して一箇所でアイコンの表示を更新する方法です。この方法では、新しい条件が追加された場合でも、一箇所の修正で済むため、コードの保守性が大幅に向上します。
複雑性の局所化と抽象化の関係
著者は、複雑性の局所化と抽象化の関係についても言及しています。適切な抽象化は複雑性を隠蔽し、コードの理解を容易にする強力なツールです。しかし、過度な抽象化は逆効果になる可能性もあります。
著者の主張する「複雑性の局所化」は、この問題に対する一つの解決策を提供していると言えるでしょう。複雑性を完全に排除するのではなく、適切に管理された形で局所化することで、システム全体の理解可能性と拡張性を維持することができます。
まとめ
著者の「Localize Complexity」という原則は、ソフトウェア開発において重要な指針を提供しています。複雑性は避けられないものですが、それを適切に管理することで、大規模で長期的なプロジェクトでも高い生産性と品質を維持することができます。
この原則は、特に近年のマイクロサービスアーキテクチャやサーバーレスコンピューティングのトレンドとも密接に関連しています。これらの技術は、大規模なシステムを小さな、管理可能な部分に分割することで、複雑性を局所化し、システム全体の柔軟性と拡張性を高めることを目指しています。
ただし、「複雑性の局所化」を追求するあまり、過度に細分化されたコンポーネントを作ってしまい、逆に全体の見通しが悪くなるというリスクもあります。適切なバランスを見出すには、継続的な実践と振り返りが必要でしょう。
最後に、この原則は特定の言語や環境に限定されるものではありません。様々なプログラミングパラダイムや開発環境において、「複雑性の局所化」という考え方は普遍的に適用できます。
Rule 11. Is It Twice as Good?
第11章「Is It Twice as Good?」は、ソフトウェア開発における重要な判断基準を提示しています。著者は、システムの大規模な変更や再設計を行う際の指針として、「新しいシステムは現行の2倍良くなるか?」という問いを投げかけています。この章を通じて、著者はソフトウェアの進化と再設計のバランス、そして変更の決定プロセスについて深い洞察を提供しています。
アーキテクチャの限界と変更の必要性
著者はまず、全てのプロジェクトが最終的にはその設計の限界に直面することを指摘しています。これは、新しい機能の追加、データ構造の変化、パフォーマンスの問題など、様々な形で現れます。
この指摘は、私の経験とも強く共鳴します。特に長期的なプロジェクトでは、当初の設計では想定していなかった要求が次々と発生し、それに対応するためにシステムを変更せざるを得なくなる状況を何度も経験してきました。
著者は、この状況に対して3つの選択肢を提示しています。
- 問題を無視する
- 小規模な調整で対応する
- 大規模なリファクタリングを行う
これらの選択肢は、実際のプロジェクトでも常に検討される事項です。しかし、著者が強調しているのは、これらの選択をどのように行うかという点です。
段階的進化vs継続的再発明
著者は、プログラマーを2つのタイプに分類しています。
- Type One:常に既存のソリューションを基に考え、問題を段階的に解決しようとするタイプ
- Type Two:問題とソリューションを一緒に考え、システム全体の問題を一度に解決しようとするタイプ
私はType Oneに近い。段階的に改善するのが性に合っている。ただ、一緒に仕事をしてきたType Twoの同僚から学んだことも多い。
著者は、どちらのタイプも極端に偏ると問題が生じると警告しています。Type Oneに偏ると、徐々に技術的負債が蓄積され、最終的にはシステムが硬直化してしまいます。一方、Type Twoに偏ると、常に一から作り直すことになり、過去の経験や知識が活かされず、進歩が遅れてしまいます。
「2倍良くなる」ルール
著者が提案する「2倍良くなる」ルールは、大規模な変更を行うかどうかを判断する際の簡潔で効果的な基準です。新しいシステムが現行の2倍良くなると確信できる場合にのみ、大規模な変更を行うべきだというこの考え方は、直感的でありながら強力です。
しかし、著者も指摘しているように、「2倍良くなる」かどうかを定量的に評価することは常に可能というわけではありません。特に、開発者の生産性や、ユーザーエクスペリエンスの向上など、定性的な改善を評価する場合は難しいケースが多々あります。
この場合、著者は可能な限り定量化を試みることを推奨しています。
小さな問題の解決機会としてのリワーク
著者は、大規模な変更を行う際には、同時に小さな問題も解決するべきだと提案しています。これは実践的なアドバイスで、私も強く共感します。
ただし、ここで注意すべきは、これらの小さな改善だけを理由に大規模な変更を行うべきではないという点です。著者の「2倍良くなる」ルールは、この判断を助ける重要な指針となります。
まとめ
この章の教訓は、ソフトウェア開発の現場で直接適用可能な、実践的なものです。特に、大規模なリファクタリングや再設計を検討する際の判断基準として、「2倍良くなる」ルールは有用です。
しかし、このルールを機械的に適用するのではなく、プロジェクトの状況や組織の文化に応じて柔軟に解釈することが重要です。
また、著者が指摘するType OneとType Twoの分類は、チーム内のバランスを考える上で有用です。多様な視点を持つメンバーでチームを構成し、お互いの強みを活かしながら決定を下していくことが、健全なソフトウェア開発につながります。
最後に、この章の教訓は、単にコードレベルの判断だけでなく、プロジェクト全体の方向性を決定する際にも適用できます。新しい技術の導入、アーキテクチャの変更、開発プロセスの改善など、大きな決断を下す際には常に「これは現状の2倍良くなるか?」という問いを念頭に置くべきでしょう。
承知しました。Rustのサンプルコードを提供し、結論を分散させた形で書き直します。
Rule 12. Big Teams Need Strong Conventions
第12章「Big Teams Need Strong Conventions」は、大規模なソフトウェア開発プロジェクトにおけるコーディング規約の重要性を深く掘り下げています。この章を通じて、著者は大規模チームでの開発における課題と、それを克服するための戦略を明確に示しています。特に、一貫したコーディングスタイルとプラクティスがチームの生産性と効率性にどのように影響するかを考察しています。
コーディング規約の必要性
著者は、プログラミングの複雑さが個人やチームの生産性を制限する主要な要因であると指摘しています。複雑さを管理し、シンプルさを維持することが、成功の鍵だと強調しています。この原則は、プロジェクトの規模や性質に関わらず適用されますが、大規模なチームでの開発においてはより重要性を増します。
大規模なチームでは、個々の開発者が「自分のコード」と「他人のコード」の境界を引こうとする傾向があります。しかし、著者はこのアプローチが長期的には機能しないと警告しています。プロジェクトが進むにつれて、コードの境界は曖昧になり、チームメンバーは常に他人のコードを読み、理解し、修正する必要が出てきます。
この状況に対処するため、著者は強力な共通のコーディング規約の必要性を主張しています。共通の規約は、コードの一貫性を保ち、チームメンバー全員がコードを容易に理解し、修正できるようにするための重要なツールです。
フォーマットの一貫性
著者は、コードのフォーマットの一貫性が重要であることを強調しています。異なるコーディングスタイルは、コードの理解を難しくし、生産性を低下させる可能性があります。Rustを使用してこの点を説明しましょう。
// 一貫性のないフォーマット(rustfmtで自動修正される) struct tree{left:Option<Box<tree>>,right:Option<Box<tree>>,value:i32} fn sum(t:&Option<Box<tree>>)->i32{match t{None=>0,Some(n)=>n.value+sum(&n.left)+sum(&n.right)}} // 一貫性のあるフォーマット struct Tree { left: Option<Box<Tree>>, right: Option<Box<Tree>>, value: i32, } fn sum(t: &Option<Box<Tree>>) -> i32 { match t { None => 0, Some(node) => node.value + sum(&node.left) + sum(&node.right), } }
これらのコードは機能的には同じですが、フォーマットが大きく異なります。一方のスタイルに慣れた開発者が他方のスタイルのコードを読む際、理解に時間がかかり、エラーを見逃す可能性が高くなります。
この問題に対処するため、著者はチーム全体で一貫したフォーマットを採用することを強く推奨しています。Rustの場合、rustfmtツールを使用することで、自動的に一貫したフォーマットを適用できます。CI/CDに組み込むことで、フォーマットの違いによるコードレビューの無駄を排除できます。
言語機能の使用規約
著者は、プログラミング言語の機能の使用方法に関する規約の重要性も強調しています。言語機能の使用方法が開発者によって異なると、コードの理解と保守が困難になります。
例えば、Rustの非同期処理とスレッドの使用を考えてみます。
use std::thread; fn sum_tree_concurrently(t: &Option<Box<Tree>>) -> i32 { match t { None => 0, Some(node) => { let left = node.left.clone(); let right = node.right.clone(); let left_handle = thread::spawn(move || sum_tree_concurrently(&left)); let right_handle = thread::spawn(move || sum_tree_concurrently(&right)); node.value + left_handle.join().unwrap() + right_handle.join().unwrap() } } }
このコードは並行処理を利用していますが、小さな木構造に対しては過剰な最適化かもしれません。Rustではスレッド生成のオーバーヘッドが明確であり、rayonクレートを使った方がより適切な並列化ができます。著者は、チーム内で言語機能の使用に関する合意を形成し、一貫して適用することの重要性を強調しています。
問題解決の規約
著者は、問題解決アプローチにも一貫性が必要だと指摘しています。同じ問題に対して異なる解決方法を用いると、コードの重複や、予期せぬ相互作用の原因となる可能性があります。
著者は、一つのプロジェクト内で複数のエラーハンドリング方法を混在させることの危険性を警告しています。チーム全体で一貫したアプローチを選択し、それを徹底することが重要です。
チームの思考の統一
著者は、効果的なチームの究極の目標を「一つの問題に対して全員が同じコードを書く」状態だと定義しています。これは単に同じフォーマットやスタイルを使用するということではなく、問題解決のアプローチ、アルゴリズムの選択、変数の命名など、あらゆる面で一貫性を持つことを意味します。
この目標を達成するために、著者は自社での実践を紹介しています。彼らは詳細なコーディング基準を設定し、コードレビューを通じてそれを徹底しています。さらに、プロジェクトの開始時にチーム全体でコーディング基準の見直しと改訂を行い、全員で合意した新しい基準を速やかに既存のコードベース全体に適用しています。
まとめ
著者の「Big Teams Need Strong Conventions」という主張は、大規模なソフトウェア開発プロジェクトの成功に不可欠な要素を指摘しています。一貫したコーディング規約は、単なる美的な問題ではなく、チームの生産性と効率性に直接影響を与える重要な要素です。
この章から学べる重要な教訓は以下の通りです。
- 大規模チームでは、個人の好みよりもチーム全体の一貫性を優先すべきである。
- コーディング規約は、フォーマット、言語機能の使用、問題解決アプローチなど、多岐にわたる要素をカバーすべきである。
- 規約は固定的なものではなく、プロジェクトの開始時や定期的に見直し、改訂する機会を設けるべきである。
- 規約の適用は、新規コードだけでなく既存のコードベース全体に及ぶべきである。
これらの原則は、特に大規模で長期的なプロジェクトにおいて重要です。一貫したコーディング規約は、新しいチームメンバーのオンボーディングを容易にし、コードの可読性と保守性を高め、結果としてプロジェクト全体の成功につながります。
以前、5人のチームで開発していたプロジェクトで、コーディング規約を定めずに進めたことがある。3ヶ月後、コードレビューで「これ誰が書いたんだ」という会話が日常化していた。同じ処理が3通りの書き方で散在し、マージのたびに衝突が起きた。
一方で、著者の主張に全面的に同意しつつも、現実のプロジェクトでの適用には課題もあると感じています。例えば、レガシーコードベースや、複数の言語やフレームワークを使用するプロジェクトでは、完全な一貫性を達成することは難しい場合があります。
また、強力な規約が個々の開発者の創造性や革新的なアプローチを抑制する可能性についても考慮する必要があります。規約の柔軟な適用と、新しいアイデアを取り入れる余地のバランスをどう取るかが、実際の開発現場での課題となるでしょう。
最後に、この章の教訓は、コーディング規約の設定と適用にとどまらず、チーム全体の文化とコミュニケーションのあり方にも及びます。規約の重要性を理解し、それを日々の開発プラクティスに組み込むためには、チーム全体の協力とコミットメントが不可欠です。
大規模チームでの開発において、強力な規約は単なる制約ではなく、チームの創造性と生産性を最大化するための重要なツールだ。
Rule 13. Find the Pebble That Started the Avalanche
第13章「Find the Pebble That Started the Avalanche」は、デバッグの本質と効果的なデバッグ手法について深く掘り下げています。著者は、プログラミングの大半がデバッグであるという現実を踏まえ、デバッグを効率化するためのアプローチを提示しています。この章を通じて、バグの原因を特定し、効果的に修正するための戦略が示されており、様々なプログラミング言語や開発環境に適用可能な普遍的な原則が提唱されています。
バグのライフサイクル
著者は、バグのライフサイクルを4つの段階に分けて説明しています。検出、診断、修正、テストです。ここで特に重要なのは診断の段階で、著者はこれを「時間旅行」になぞらえています。つまり、問題が発生した瞬間まで遡り、そこから一歩ずつ追跡していく過程です。
この考え方は、私の経験とも強く共鳴します。例えば、以前担当していた決済システムで、特定の条件下でのみ発生する不具合があり、その原因を特定するのに苦労した経験があります。結局、トランザクションログを詳細に分析し、問題の発生時点まで遡ることで、原因を特定できました。
著者が提唱する「時間旅行」的アプローチは、特に複雑なシステムでのデバッグに有効です。例えば、Rustを使用したマイクロサービスアーキテクチャにおいて、以下のようなコードで問題が発生した場合を考えてみます。
async fn process_payment(payment: &Payment) -> Result<(), PaymentError> { validate_payment(payment)?; deduct_balance(payment.user_id, payment.amount).await?; // ここでcreate_transactionが失敗した場合、 // deduct_balanceのロールバックが必要だが、されていない create_transaction(payment).await?; Ok(()) }
このコードでは、create_transactionが失敗した場合にロールバックが行われていません。Rustの?演算子はエラー伝播を簡潔にしますが、補償トランザクションの実装を忘れやすくする罠でもあります。このバグを発見した場合、著者の提唱する方法に従えば、まず問題の症状(不整合な状態のデータ)から始めて、一歩ずつ遡っていくことになります。
原因と症状の関係
著者は、バグの原因と症状の関係性について深く掘り下げています。多くの場合、症状が現れた時点ですでに原因からは遠く離れていることを指摘し、この「距離」がデバッグを困難にしていると説明しています。
この洞察は重要で、私もしばしば経験します。例えば、メモリリークのようなバグは、症状(アプリケーションの異常な遅延や停止)が現れた時点で、既に原因(特定のオブジェクトが適切に解放されていないこと)から何時間も経過していることがあります。
著者は、この問題に対処するために、できるだけ早く問題を検出することの重要性を強調しています。これは、例えばRustのtokioランタイムとキャンセレーショントークンを使用して、長時間実行される処理を監視し、早期に異常を検出するようなアプローチにつながります。
use tokio::select; use tokio_util::sync::CancellationToken; async fn long_running_process(cancel_token: CancellationToken) -> Result<(), ProcessError> { loop { select! { _ = cancel_token.cancelled() => { return Err(ProcessError::Cancelled); } result = do_something() => { result?; } } } }
このように、キャンセレーショントークンを使用することで、処理の異常な長期化や、親プロセスからのキャンセル指示を即座に検出できます。Rustのselect!マクロは複数の非同期操作を同時に待機し、最初に完了したものを処理します。
ステートの最小化
著者は、デバッグを容易にするための重要な戦略として、ステート(状態)の最小化を強調しています。純粋関数(pure function)の使用を推奨し、これがデバッグを著しく容易にすると主張しています。
この点については、強く同意します。例えば、以前担当していた在庫管理システムでは、ステートフルなコードが多く、デバッグに多大な時間を要していました。そこで、可能な限り純粋関数を使用するようにリファクタリングしたところ、バグの特定と修正が格段に容易になりました。
Rustでの具体例を示すと、以下のようになります。
use std::collections::HashMap; // ステートフルな実装 struct Inventory { items: HashMap<String, i32>, } impl Inventory { fn add_item(&mut self, item_id: &str, quantity: i32) { *self.items.entry(item_id.to_string()).or_insert(0) += quantity; } } // 純粋関数を使用した実装 fn add_item(items: &HashMap<String, i32>, item_id: &str, quantity: i32) -> HashMap<String, i32> { let mut new_items = items.clone(); *new_items.entry(item_id.to_string()).or_insert(0) += quantity; new_items }
Rustでは&mut selfにより可変性が明示されるため、ステートフルな実装でも副作用の範囲が型レベルで明確です。純粋関数を使用した実装では、同じ入力に対して常に同じ出力が得られるため、デバッグが容易になります。
避けられないステートへの対処
著者は、完全にステートレスなコードを書くことは現実的ではないことを認識しつつ、避けられないステートに対処する方法についても言及しています。特に印象的だったのは、「実行可能なログファイル」という概念です。
この考え方は、私が以前取り組んでいた分散システムのデバッグに役立ちました。システムの状態を定期的にスナップショットとして保存し、問題が発生した時点のスナップショットを使ってシステムを再現することで、複雑なバグの原因を特定することができました。
Rustでこのアプローチを実装する例を示します。
use tracing::{error, instrument}; #[derive(Debug, Clone)] struct SystemState { // システムの状態を表す構造体 } fn capture_state() -> SystemState { // 現在のシステム状態をキャプチャ SystemState {} } fn replay_state(_state: &SystemState) { // キャプチャした状態を再現 } #[instrument(skip(req))] fn process_request(req: Request) -> Response { let initial_state = capture_state(); let resp = process_request_internal(req); if let Err(ref e) = resp.result { error!(?initial_state, error = %e, "Error occurred"); } resp }
Rustではtracingクレートを使うことで、構造化ログとスパンベースのトレーシングが可能です。#[instrument]属性により、関数の入出力を自動的に記録できます。このアプローチを採用することで、複雑なステートを持つシステムでも、バグの再現と診断が容易になります。
まとめ
著者の「雪崩を引き起こした小石を見つけよ」という原則は、効果的なデバッグの本質を捉えています。症状の単なる修正ではなく、根本原因の特定と修正の重要性を強調しているのが印象的です。
要するに、デバッグは「バグ修正」ではなくシステムを理解するプロセスだ。時間はかかる。でも、その時間がコードの品質と自分の理解を深める。
私自身、この原則を実践することで、単にバグを修正するだけでなく、システム全体の設計や実装の改善にもつながった経験があります。例えば、あるマイクロサービスでのバグ修正をきっかけに、サービス間の通信プロトコルを見直し、全体的なシステムの堅牢性を向上させることができました。
著者の提案する「時間旅行」的デバッグアプローチは、特に分散システムやマイクロサービスアーキテクチャのような複雑な環境で有効です。これらのシステムでは、問題の原因と症状が時間的・空間的に大きく離れていることが多いため、著者の提案するアプローチは貴重な指針となります。
最後に、この章の教訓は、単にデバッグ技術の向上にとどまらず、より良いソフトウェア設計につながる。ステートの最小化や純粋関数の使用といった原則は、バグの発生自体を減らし、システム全体の品質を向上させる。
Rule 14. Code Comes in Four Flavors
第14章「Code Comes in Four Flavors」は、プログラミングの問題と解決策を4つのカテゴリーに分類し、それぞれの特徴と重要性を深く掘り下げています。著者は、Easy問題とHard問題、そしてそれらに対するSimple解決策とComplicated解決策という枠組みを提示し、これらの組み合わせがプログラマーの技量をどのように反映するかを論じています。この章を通じて、コードの複雑さと単純さのバランス、そしてそれがソフトウェア開発の質と効率にどのように影響するかが明確に示されています。
4つのコードの味
著者は、プログラミングの問題を「Easy」と「Hard」の2種類に大別し、さらにそれぞれの解決策を「Simple」と「Complicated」に分類しています。この枠組みは一見単純ですが、実際のプログラミング現場での課題をよく反映していると感じました。
特に印象的だったのは、Easy問題に対するComplicated解決策の危険性への指摘です。私自身、過去のプロジェクトで、単純な問題に対して過度に複雑な解決策を実装してしまい、後々のメンテナンスで苦労した経験があります。例えば、単純なデータ処理タスクに対して、汎用性を追求するあまり複雑なクラス階層を設計してしまい、結果的にコードの理解と修正が困難になった事例が思い出されます。
著者の主張する「Simple解決策の重要性」は、現代のソフトウェア開発においても重要です。特に、マイクロサービスアーキテクチャやサーバーレスコンピューティングが主流となっている現在、個々のコンポーネントの単純さと明確さがシステム全体の健全性に大きく影響します。
複雑さのコスト
著者は、不必要な複雑さがもたらす実際のコストについて詳しく論じています。複雑なコードは書くのに時間がかかり、デバッグはさらに困難になるという指摘は、私の経験とも強く共鳴します。
例えば、以前参画していた大規模プロジェクトでは、初期段階で採用された過度に抽象化された設計が、プロジェクトの後半で大きな足かせとなりました。新機能の追加や既存機能の修正に予想以上の時間がかかり、結果的にプロジェクト全体のスケジュールに影響を与えてしまいました。
この経験から、私は「単純さ」を設計の重要な指標の一つとして意識するようになりました。例えば、Rustを使用する際は、言語自体が持つ表現力を活かしつつ、以下のような原則を心がけています。
// 複雑な例 struct DataProcessor { data: Vec<i32>, // 多数のフィールドと複雑なロジック } impl DataProcessor { fn process(&self) -> i32 { // 複雑で理解しづらい処理 0 } } // シンプルな例 fn process_data(data: &[i32]) -> i32 { data.iter().sum() }
シンプルな関数は理解しやすく、テストも容易です。Rustのイテレータを使えば、意図がより明確に表現されます。これは、著者が主張する「Simple解決策」の具体例と言えるでしょう。
プログラマーの3つのタイプ
著者は、問題の難易度と解決策の複雑さの組み合わせに基づいて、プログラマーを3つのタイプに分類しています(Mediocre,Good,Great)。この分類は興味深く、自身のスキルレベルを客観的に評価する良い指標になると感じました。
特に、「Great」プログラマーがHard問題に対してもSimple解決策を見出せるという指摘は、プロフェッショナルとしての目標設定に大きな示唆を与えてくれます。これは、単に技術的なスキルだけでなく、問題の本質を見抜く洞察力や、複雑な要求をシンプルな形に落とし込む能力の重要性を示唆しています。
実際の開発現場では、この「Great」プログラマーの特性が如実に現れる場面があります。例えば、システムの設計段階で、複雑な要件を整理し、シンプルかつ拡張性のある設計を提案できる能力は価値があります。
私自身、この「Great」プログラマーを目指して日々精進していますが、Hard問題に対するSimple解決策の発見は常に挑戦的です。例えば、分散システムにおけるデータ一貫性の問題など、本質的に複雑な課題に対して、いかにシンプルで堅牢な解決策を見出すかは、常に頭を悩ませる問題です。
Hard問題のSimple解決策
著者は、Hard問題に対するSimple解決策の例として、文字列の順列検索問題を取り上げています。この例は、問題の捉え方を変えることで、複雑な問題に対してもシンプルな解決策を見出せることを示しており、示唆に富んでいます。
著者が示した最終的な解決策は、問題の本質を捉え、不要な複雑さを排除した素晴らしい例だと感じました。このアプローチは、実際の開発現場でも有用です。
まとめ
この章から得られる最も重要な教訓は、コードの単純さと明確さが、プログラマーの技量を示すということです。Easy問題に対するSimple解決策を見出せることは良いプログラマーの証ですが、Hard問題に対してもSimple解決策を提案できることが、真に優れたプログラマーの特徴だという著者の主張には強く共感します。
この原則は、日々のコーディングからアーキテクチャ設計まで、あらゆる場面で意識すべきものだ。
技術は変わる。フレームワークは入れ替わる。でも「シンプルさ」という原則は普遍的だ。
Rule 15. Pull the Weeds
第15章「Pull the Weeds」は、コードベースの健全性維持に関する重要な原則を提示しています。著者は、小さな問題や不整合を「雑草」に例え、それらを放置せずに定期的に除去することの重要性を強調しています。この章を通じて、コードの品質維持がソフトウェア開発プロセス全体にどのように影響するか、そして日々の開発作業の中でどのようにこの原則を実践すべきかが明確に示されています。
雑草とは何か
著者は、Animal Crossingというゲームの雑草除去の例を用いて、コードの「雑草」の概念を説明しています。この比喩は的確で、私自身、長年のソフトウェア開発経験を通じて、まさにこのような「雑草」の蓄積がプロジェクトの進行を妨げる様子を何度も目の当たりにしてきました。
著者が定義する「雑草」は、修正が容易で、放置しても大きな問題にはならないが、蓄積すると全体の品質を低下させる小さな問題です。具体的には、コメントの誤字脱字、命名規則の不一致、フォーマットの乱れなどが挙げられています。
この定義は重要で、多くの開発者が見落としがちな点だと感じます。例えば、以前私が参画していた大規模プロジェクトでは、コーディング規約の軽微な違反を「些細な問題」として放置していました。結果として、コードの一貫性が失われ、新規メンバーの学習コストが増大し、最終的にはプロジェクト全体の生産性低下につながりました。
雑草の除去
著者は、雑草の除去プロセスを段階的に示しています。最初に、明らかな誤りや不整合を修正し、次に命名規則やフォーマットの統一を行います。この段階的アプローチは実践的で、日々の開発作業に組み込みやすいと感じました。
例えば、著者が示したC++のコード例では、関数名の変更(エクスポートするための大文字化)、変数名の明確化、コメントの追加と修正、フォーマットの統一などが行われています。これらの変更は、コードの機能自体には影響を与えませんが、可読性と保守性を大きく向上させます。
雑草の特定
著者は、ある問題が「雑草」であるかどうかを判断する基準として、修正の安全性を挙げています。コメントの修正や命名規則の統一など、機能に影響を与えない変更は安全に行えるため、「雑草」として扱うべきだと主張しています。
この考え方は、現代のソフトウェア開発プラクティス、特に継続的インテグレーション(CI)と継続的デリバリー(CD)の文脈で重要です。例えば、私のチームでは、linterやフォーマッターをCIパイプラインに組み込むことで、多くの「雑草」を自動的に検出し、修正しています。これにより、人間の判断が必要な、より重要な問題に集中できるようになりました。
コードが雑草だらけになる理由
著者は、多くのプロジェクトで「雑草」が放置される理由について深く掘り下げています。時間の制約、優先順位の問題、チーム内での認識の違いなど、様々な要因が挙げられています。
この分析は的確で、私自身も同様の経験があります。特に印象に残っているのは、あるプロジェクトでチーム全体が「完璧主義に陥らないこと」を重視するあまり、小さな問題を軽視する文化が生まれてしまったことです。結果として、コードの品質が徐々に低下し、最終的には大規模なリファクタリングが必要になりました。
まとめ
著者は、「雑草を抜く」ことの重要性を強調して章を締めくくっています。小さな問題を放置せず、定期的に対処することが、長期的にはプロジェクトの健全性を維持する上で重要だと主張しています。
この主張には強く共感します。私の経験上、コードの品質維持は継続的な取り組みが必要で、一度に大規模な修正を行うよりも、日々の小さな改善の積み重ねの方が効果的です。
例えば、私のチームでは「雑草抜きの金曜日」という取り組みを始めました。毎週金曜日の午後2時間を、コードベースの小さな改善に充てるのです。この取り組みにより、コードの品質が向上しただけでなく、チームメンバー全員がコードベース全体に対する理解を深めることができました。
最後に、著者の「最も経験豊富なチームメンバーが雑草抜きの先頭に立つべき」という提案は、重要なポイントだと感じます。ベテラン開発者が率先して小さな問題に対処することで、その重要性をチーム全体に示すことができます。また、そのプロセスを通じて、若手開発者に暗黙知を伝えることもできるのです。
結局、コードの品質維持は日々の小さな努力の積み重ねだ。「雑草を抜く」という地味な行為が、長期的にはプロジェクトの成否を分ける。
Rule 16. Work Backward from Your Result, Not Forward from Your Code
第16章「Work Backward from Your Result, Not Forward from Your Code」は、ソフトウェア開発における問題解決アプローチの根本的な転換を提案しています。著者は、既存のコードや技術から出発するのではなく、望む結果から逆算してソリューションを構築することの重要性を説いています。この原則は、言語や技術に関わらず適用可能です。
プログラミングは橋を架ける行為
著者はプログラミングを、既存のコードと解決したい問題の間に「橋を架ける」行為に例えています。この比喩は示唆に富んでいます。日々の開発作業を振り返ると、確かに我々は常に既知の技術と未知の問題の間を行き来しているように感じます。
しかし、著者が指摘するように、多くの場合我々は「コードの側」に立って問題を見ています。つまり、手持ちの技術やライブラリの視点から問題を捉えようとしがちなのです。これは、ある意味で自然な傾向かもしれません。既知の領域から未知の領域に進むのは、心理的にも安全に感じられるからです。
既存のコードの視点で捉える危険性
著者は、この「コードの側」から問題を見るアプローチの危険性を指摘しています。この指摘は重要で、私自身も頻繁に陥りがちな罠だと感じています。
例えば、設定ファイルの解析という問題に直面したとき、多くの開発者はすぐにJSONやYAMLといった既存のフォーマットを思い浮かべるでしょう。そして、それらを解析するための既存のライブラリを探し始めます。これは一見効率的に見えますが、実際には問題の本質を見失うリスクがあります。
設定ファイルの真の目的は何でしょうか?それは、アプリケーションの動作を柔軟に調整することです。しかし、既存のフォーマットやライブラリに頼りすぎると、その本質的な目的よりも、特定のフォーマットの制約に縛られてしまう可能性があります。
結果から逆算するアプローチ
著者が提案する「結果から逆算する」アプローチは、この問題に対する解決策です。まず、理想的な設定の使用方法を想像し、そこから逆算して実装を考えるのです。
例えば、設定ファイルの問題に対して、以下のような理想的な使用方法を想像できるでしょう:
- 設定値へのアクセスが型安全である
- デフォルト値が簡単に設定できる
- 環境変数からの上書きが容易である
- 設定値の変更を検知できる
理想的な使用方法を定義してから実装を始めることで、より使いやすく、保守性の高い設定システムを設計できる可能性が高まります。
型安全性と抽象化
著者は、型安全性と適切な抽象化の重要性も強調しています。これは特にRustのような静的型付け言語で重要です。Rustの強力な型システムは、コンパイル時に多くのエラーを検出できるため、この原則との相性が良いです。
例えば、設定値に対して単純な文字列や数値の型を使うのではなく、それぞれの設定値の意味や制約を表現する独自の型を定義することが考えられます。これにより、コンパイル時のエラーチェックが可能になり、実行時のエラーを減らすことができます。
まとめ
著者は、既存の技術から前進するアプローチと、望む結果から後退するアプローチの両方を探求しています。どちらか一方だけが正しいわけではなく、状況に応じて適切なアプローチを選択することが重要です。
核心は単純だ。まず望む結果を明確にし、そこから逆算する。コードから始めるな、ゴールから始めろ。
この原則を意識することで、単に既存の技術を組み合わせるだけでなく、本当に問題を解決するソリューションを生み出せる可能性が高まります。
Rule 17. Sometimes the Bigger Problem Is Easier to Solve
第17章「Sometimes the Bigger Problem Is Easier to Solve」は、問題解決のアプローチに新たな視点を提供しています。この章では、一見複雑に見える問題に対して、より大きな視点から取り組むことで、意外にもシンプルな解決策を見出せる可能性があることを説いています。
問題の規模と複雑さの関係
著者は、プログラマーがしばしば直面する困難な状況として、特定の問題に対する解決策を見出そうとするものの、その問題自体が複雑すぎて手に負えないように感じられるケースを挙げています。これは、多くの開発者が経験したことのある状況だと思います。私自身も、マイクロサービスアーキテクチャの設計や分散システムのデータ同期など、一見すると複雑な問題に直面し、最初のアプローチを何度も書き直した経験があります。
しかし、著者が提案するアプローチは、この状況で有効です。問題の規模を拡大し、より一般的な視点から捉え直すことで、意外にもシンプルな解決策が見つかることがあるというのです。これは、森を見るために木から離れる必要があるという格言を思い起こさせます。
この原則は、日々の開発業務においても有用です。例えば、あるマイクロサービスの特定のエンドポイントのパフォーマンス最適化に苦心している場合、その個別の問題に固執するのではなく、サービス全体のアーキテクチャを見直すことで、より効果的な解決策が見つかることがあります。
抽象化と一般化の重要性
著者の主張する「より大きな問題を解決する」アプローチは、多くのプログラミング言語や開発手法の設計哲学とも相性が良いと感じます。インターフェースを通じた抽象化や、ジェネリクスを用いた一般化などの機能は、まさにこの原則を実践するのに適しています。
例えば、複数のデータソースから情報を取得し、それを集約して処理するという問題を考えてみましょう。最初のアプローチでは、各データソースに対して個別の処理を書き、それぞれの結果を手動で集約しようとするかもしれません。しかし、問題をより大きな視点から捉え直すと、これらのデータソースを抽象化し、共通のインターフェースを通じてアクセスするという解決策が浮かび上がります。
このアプローチでは、個々のデータソースの詳細を抽象化し、より一般的な問題(複数のデータソースからのデータ取得と集約)に焦点を当てています。結果として、コードはより簡潔になり、新しいデータソースの追加も容易になります。
実務での適用
私の経験では、あるプロジェクトで複数のマイクロサービス間のデータ整合性の問題に直面したことがあります。当初は各サービス間の個別の同期メカニズムの実装に注力していましたが、問題を大きく捉え直すことで、イベントソーシングパターンを採用するという解決策にたどり着きました。これにより、個別の同期ロジックの複雑さを大幅に軽減し、システム全体の一貫性と拡張性を向上させることができました。
このアプローチは、単にコードレベルの問題だけでなく、システム設計全体にも適用できます。例えば、複雑なビジネスロジックを持つアプリケーションの開発において、個々の機能ごとに独立したモジュールを作成するのではなく、最小限のクリーンアーキテクチャを採用することで、より一貫性のあるシステム設計が可能になることがあります。これにより、ビジネスロジックとインフラストラクチャの関心事を分離しつつ、過度に複雑化することなくシステムの構造を整理できます。
批判的考察
著者の提案するアプローチは魅力的ですが、いくつかの注意点も考慮する必要があります。まず、問題を大きく捉え過ぎると、実装が過度に一般化され、具体的なユースケースに対する最適化が困難になる可能性があります。また、チームのスキルセットや既存のコードベースとの整合性など、実務的な制約も考慮する必要があります。
例えば、データソースの抽象化の例で、過度に抽象化されたインターフェースを導入することで、個々のデータソースの特性を活かした最適化が難しくなる可能性があります。この場合、抽象化のレベルをどこに設定するかは慎重に検討する必要があります。
また、大きな問題を解決するアプローチを採用する際は、チーム全体の理解と合意が必要です。個々の開発者が局所的な最適化に注力している状況で、突然大規模な設計変更を提案すると、チームの混乱を招く可能性があります。そのため、このアプローチを採用する際は、十分なコミュニケーションとチーム全体の理解が不可欠です。
まとめ
「Sometimes the Bigger Problem Is Easier to Solve」という原則は、ソフトウェア開発において有用な視点を提供しています。複雑な問題に直面したとき、その問題自体に固執するのではなく、一歩引いて大局的な視点から捉え直すことで、より簡潔で汎用的な解決策を見出せる可能性があります。
この原則を適用することで、個別の問題に対する局所的な解決策ではなく、システム全体の設計と一貫性を改善するチャンスが得られます。これは、長期的にはコードの保守性や拡張性の向上につながり、プロジェクト全体の健全性に貢献します。
しかし、この原則を適用する際は、具体的なユースケースとのバランスを常に意識する必要があります。過度の一般化は避け、プロジェクトの要件や制約を十分に考慮した上で、適切な抽象化のレベルを選択することが重要です。
最後に、この原則は単にコーディングの技術だけでなく、問題解決のアプローチ全般に適用できる重要な考え方です。ソフトウェア開発者として、常に大局的な視点を持ち、問題の本質を見極める努力を続けることが、より効果的で持続可能なソリューションの創出につながるのです。
複雑な問題に直面したときこそ、一歩引いて大きな視点から問題を捉え直す。木を見て森を見ず、という状態から抜け出す勇気が必要だ。
Rule 18. Let Your Code Tell Its Own Story
第18章「Let Your Code Tell Its Own Story」は、コードの可読性と自己説明性に焦点を当てています。この章を通じて、著者は良いコードが自らの物語を語るべきだという重要な原則を提示しています。コードの可読性が高まると、開発効率が向上し、バグの発見も容易になります。この原則は、言語や技術に関わらず適用可能です。
コードの可読性の重要性
著者は、コードの可読性を向上させることの重要性を強調しています。これは、私たちがコードを書く時間よりも読む時間の方が圧倒的に長いという現実を考えると、重要な指摘です。特に、チーム開発やオープンソースプロジェクトでは、他の開発者がコードを理解しやすいかどうかが、プロジェクトの成功を左右する重要な要因となります。
私自身、過去のプロジェクトで、可読性の低いコードに悩まされた経験があります。例えば、ある大規模なマイクロサービスプロジェクトでは、各サービスの責任範囲が明確でなく、コードの意図を理解するのに多大な時間を要しました。この経験から、コードの自己説明性の重要性を痛感しました。
コメントの役割と落とし穴
著者は、コメントの重要性を認めつつも、その使用には注意が必要だと指摘しています。特に、誤ったコメントや古くなったコメントが、コードの理解を妨げる可能性があることを強調しています。
この点は、日々の開発でよく遭遇する問題です。例えば、以前参画していたプロジェクトでは、コメントとコードの内容が一致しておらず、デバッグに多大な時間を要したことがありました。この経験から、コメントは最小限に抑え、コード自体が意図を明確に表現するよう心がけるべきだと学びました。
Rustの文脈では、言語自体が読みやすさと型による自己文書化を重視しているため、過度なコメントは逆効果になる可能性があります。例えば、以下のようなコードは、コメントなしでも十分に意図が伝わります:
fn is_even(num: i32) -> bool { num % 2 == 0 }
この単純な関数に対してコメントを追加するのは、かえって可読性を下げる可能性があります。Rustでは戻り値の型が明示されるため、関数のシグネチャ自体がドキュメントの役割を果たします。
命名の重要性
著者は、適切な命名の重要性を強調しています。これは、コードの自己説明性を高める上で最も重要な要素の一つです。
私の経験上、適切な命名は、コードレビューの効率を大幅に向上させます。例えば、あるプロジェクトでは、変数名や関数名の命名規則を厳格に定め、チーム全体で遵守しました。その結果、コードの理解とレビューにかかる時間が大幅に削減されました。
Rustでは、可視性がpubキーワードにより明示的に制御されます。これにより、コードの意図がより明確になります:
pub struct User { pub id: i32, // 公開フィールド name: String, // プライベートフィールド(デフォルト) } impl User { pub fn new(id: i32, name: String) -> Self { Self { id, name } } pub fn name(&self) -> &str { &self.name // ゲッターで制御されたアクセス } }
Rustではフィールドのデフォルトがプライベートであり、公開する場合は明示的にpubを付ける必要があります。これにより、カプセル化が言語レベルで強制されます。
コードの構造化と整形
著者は、コードの構造化と整形の重要性についても言及しています。適切に構造化されたコードは、読み手にとって理解しやすくなります。
この点について、Rustはrustfmtというツールを提供しており、コードの自動整形を行うことができます。これにより、チーム全体で一貫したコードスタイルを維持することが容易になります。
まとめ
「Let Your Code Tell Its Own Story」という原則は、現代のソフトウェア開発において重要です。特に、チーム開発やオープンソースプロジェクトでは、コードの可読性と自己説明性が、プロジェクトの成功を左右する重要な要因となります。
Rustの文脈では、言語自体が読みやすさと安全性を重視しているため、この原則を適用しやすい環境が整っていると言えます。型システムが自己文書化の役割を果たし、コンパイラが多くのミスを事前に指摘してくれます。しかし、それでも開発者の意識的な努力が必要です。
最後に、この原則は単にコーディングスキルの向上だけでなく、チームのコミュニケーションの改善にもつながります。コードが自らの物語を語ることができれば、チームメンバー間の理解が深まり、結果としてプロジェクト全体の生産性が向上するでしょう。
コードは他の開発者(そして未来の自分)に向けて書くものだ。3ヶ月後の自分は他人だと思え。
Rule 19. Rework in Parallel
第19章「Rework in Parallel」は、大規模なコードベースの改修に関する重要な戦略を提示しています。著者は、並行して新旧のシステムを動作させることで、リスクを最小限に抑えつつ段階的に改修を進める方法を詳細に解説しています。この章を通じて、著者は大規模なリファクタリングや機能追加におけるベストプラクティスを示し、ソフトウェア開発の現場で直面する現実的な課題に対する洞察を提供しています。
並行リワークの必要性
著者は、大規模なコードベースの改修が必要となる状況から話を始めています。例えば、チームでの開発や、長期にわたるプロジェクトでは、単純な「チェックアウト→修正→コミット」のモデルでは対応しきれない場合があります。特に、他の開発者との協業が必要な場合や、改修作業が長期化する場合には、従来のブランチモデルでは様々な問題が発生する可能性があります。
私自身、過去に大規模なマイクロサービスのリアーキテクチャプロジェクトに携わった際、長期間のブランチ作業による問題を経験しました。メインブランチとの統合が困難になり、結果として予定以上の時間とリソースを要してしまいました。著者の指摘する問題点は、現実のプロジェクトでも頻繁に発生する課題だと強く共感します。
並行システムの構築
著者が提案する解決策は、新旧のシステムを並行して動作させる「duplicate-and-switch」モデルです。この方法では、既存のシステムを変更する代わりに、並行システムを構築します。新システムは開発中でもメインブランチにコミットされますが、ランタイムスイッチによって制御され、最初は小規模なチームでのみ使用されます。
このアプローチは、Kent Beckの「For each desired change, make the change easy (warning: this may be hard), then make the easy change」という格言を大規模プロジェクトに適用したものと言えます。私も以前、レガシーシステムの段階的な置き換えプロジェクトで類似のアプローチを採用しましたが、確かにリスクを抑えつつ改修を進められた経験があります。
具体例:スタックベースのメモリアロケータ
著者は、具体例としてスタックベースのメモリアロケータの改修を挙げています。この例は、低レベルのシステムコンポーネントの改修という点で興味深いものです。スタックベースのアロケーションは、高速で効率的なメモリ管理を可能にしますが、同時に複雑な課題も抱えています。
著者が示した問題点、特に異なるスタックコンテキスト間での操作の困難さは、私が以前関わった分散システムのメモリ管理でも直面した課題です。この種の問題は、単純なリファクタリングでは解決が難しく、システム全体の再設計が必要になることがあります。
並行リワークの実践
著者は、並行リワークの実践方法を段階的に説明しています。特に印象的だったのは、以下の点です:
- 新旧のシステムを切り替えるためのグローバルフラグの導入
- アダプタクラスを使用した新旧システムの橋渡し
- 段階的な移行と継続的なテスト
このアプローチは、リスクを最小限に抑えつつ大規模な変更を行うための優れた戦略だと感じました。私自身、似たようなアプローチを採用してデータベースシステムの移行を行った経験がありますが、確かに安全性と柔軟性の両立に効果的でした。
並行リワークの適用タイミング
著者は、並行リワークが常に最適な解決策ではないことも指摘しています。この戦略はオーバーヘッドを伴うため、適用するタイミングと状況を慎重に見極める必要があります。
私見では、以下のような状況で並行リワークが特に有効だと考えます:
一方で、小規模な変更や短期的なプロジェクトでは、従来のブランチモデルの方が適している場合もあります。
まとめ
「Rework in Parallel」の原則は、大規模なソフトウェア開発プロジェクトにおいて重要な戦略を提供しています。この手法を適切に適用することで、リスクを最小限に抑えつつ、大規模な改修や機能追加を実現できます。
著者の提案するアプローチは、現代の開発環境、特にマイクロサービスアーキテクチャやクラウドネイティブ開発において有用です。例えば、新旧のサービスを並行して稼働させ、トラフィックを段階的に移行するような戦略は、この原則の自然な拡張と言えるでしょう。
しかし、この手法を適用する際は、プロジェクトの規模や性質、チームの状況などを十分に考慮する必要があります。また、並行リワークを成功させるためには、強力な自動化テストやCI/CDパイプライン、モニタリングシステムなどの支援が不可欠です。
この手法は短期的には追加の労力が必要だ。でも、長期的にはテクニカルデットの削減とシステムの健全性維持に寄与する。
大規模な変更を行う際は、リスクを分散させ、段階的にアプローチする。一気にやりたい衝動を抑えろ。
Rule 20. Do the Math
第20章「Do the Math」は、プログラミングにおける数学的思考の重要性を強調しています。著者は、多くのプログラミングの決定が定性的なものである一方で、数学的な分析が有効な場面も多々あることを指摘しています。この章を通じて、著者は単純な計算が問題解決のアプローチの妥当性を検証する上で、いかに重要であるかを具体的な例を挙げながら説明しています。
自動化の判断
著者は、タスクの自動化を例に挙げ、数学的思考の重要性を説明しています。自動化するかどうかの判断は、単純な数学の問題に帰着します。コードを書くのにかかる時間と、手動でタスクを繰り返す時間を比較し、前者の方が短ければ自動化する価値があるというわけです。
この考え方は一見当たり前に思えますが、実際の開発現場ではこの単純な計算が軽視されがちです。私自身、過去のプロジェクトで、チームメンバーが十分な検討もなしに自動化に走り、結果として無駄な工数を費やしてしまった経験があります。
著者の指摘する「自動化の判断」は、特にデプロイメントプロセスやテスト自動化の文脈で重要です。例えば、CI/CDパイプラインの構築を検討する際、その構築コストと、手動デプロイメントにかかる時間を比較検討することが重要です。ただし、この計算には定量化しづらい要素(例:人的ミスの削減、チームの士気向上)も含まれるため、純粋な数学だけでなく、総合的な判断が必要になります。
ハードリミットの重要性
著者は、問題空間や解決策におけるハードリミット(固定的な制約)の重要性を強調しています。ゲーム開発を例に、メモリ容量やネットワーク帯域幅などの制約が、設計プロセスにおいて重要な役割を果たすことを説明しています。
この考え方は、ゲーム開発に限らず、多くのソフトウェア開発プロジェクトに適用できます。例えば、マイクロサービスアーキテクチャを採用する際、各サービスのリソース制限(CPU、メモリ、ネットワーク帯域)を明確に定義し、それに基づいてシステム設計を行うことが重要です。
著者の提案する「ハードリミットの設定」は、特にパフォーマンスクリティカルなシステムの設計において有効です。例えば、高頻度取引システムの設計では、レイテンシの上限を明確に定義し、それを満たすようなアーキテクチャを検討することが重要です。
数学の変化への対応
著者は、要件の変更に伴い、数学的な計算も再評価する必要があることを指摘しています。これは、アジャイル開発の文脈で特に重要です。要件が頻繁に変更される環境では、定期的に数学的な再評価を行い、アプローチの妥当性を確認することが重要です。
例えば、スケーラビリティを考慮したシステム設計において、想定ユーザー数や処理データ量が変更された場合、それに応じてインフラストラクチャのキャパシティプランニングを再計算する必要があります。
定量的分析から定性的判断へ
著者は、純粋な数学的アプローチだけでなく、定性的な要素も考慮することの重要性を強調しています。例えば、タスクの自動化において、時間の節約だけでなく、エラーの削減やチームの満足度向上といった定性的な要素も考慮に入れる必要があります。
この考え方は、技術的負債の管理にも適用できます。リファクタリングの判断において、純粋なコスト計算だけでなく、コードの可読性向上やメンテナンス性の改善といった定性的な要素も考慮に入れる必要があります。
まとめ
「Do the Math」の原則は、ソフトウェア開発における意思決定プロセスに数学的思考を取り入れることの重要性を強調しています。この原則は、特に大規模で複雑なシステムの設計や、リソース制約のある環境での開発において有用です。
著者の提案するアプローチは、現代の開発環境、特にクラウドネイティブ開発やマイクロサービスアーキテクチャにおいて重要です。リソースの最適化、コストの最小化、パフォーマンスの最大化といった課題に直面する際、数学的な分析は不可欠です。
しかし、純粋な数学だけでなく、定性的な要素も考慮に入れることの重要性も忘れてはいけません。ソフトウェア開発は単なる数字の問題ではなく、人間の創造性や協力関係が重要な役割を果たす分野です。
私自身の経験を踏まえると、この原則は特にパフォーマンスチューニングやシステム設計の場面で有用です。例えば、データベースのインデックス設計やキャッシュ戦略の検討において、数学的な分析は不可欠でした。同時に、チームの習熟度や保守性といった定性的な要素も考慮に入れることで、より良い意思決定ができました。
数学的思考と定性的判断のバランスを取る。計算できるものは計算しろ。ただし、数字にならないものを無視するな。
Rule 21. Sometimes You Just Need to Hammer the Nails
第21章「Sometimes You Just Need to Hammer the Nails」は、プログラミングにおける地道な作業の重要性を強調しています。著者は、創造的で知的な挑戦が多いプログラミングの世界でも、時には単純で退屈な作業が必要不可欠であることを説いています。この章を通じて、著者は「面倒な作業を避けない」ことの重要性と、それがソフトウェア開発プロジェクト全体にどのような影響を与えるかを明確に示しています。
本書の最後にこのような泥臭い作業の重要性を説くルールを紹介しているのは、この書籍の優れた点の一つだと言えるでしょう。この章は、プログラミングの現実的な側面を忘れずに、理想と実践のバランスを取ることの大切さを読者に印象づけています。
プログラマーの三大美徳との関連
この章の内容は、かつてよく知られていた「プログラマーの三大美徳」と密接に関連しています。これらの美徳は「怠慢」「短気」「傲慢」であり、一見ネガティブに聞こえますが、実際には優れたプログラマーの特質を表しています。
怠慢:全体の労力を減らすために手間を惜しまない気質。例えば、繰り返し作業を自動化したり、再利用可能なコンポーネントを作成したりすることで、長期的な効率を向上させます。
短気:コンピューターの非効率さに対する怒り。この特質は、現在の問題だけでなく、将来起こりうる問題も予測して対応しようとする姿勢につながります。
傲慢:自分のコードに対する高い誇りと責任感。これは、保守性や可読性、柔軟性の高いコードを書こうとする姿勢に現れます。
これらの美徳は、「Sometimes You Just Need to Hammer the Nails」の原則と補完的な関係にあります。地道な作業を避けないことは、長期的には「怠慢」な姿勢(良い意味で)につながり、「短気」な気質は将来の問題を予見して対処することを促します。そして、「傲慢」さは、たとえ退屈な作業であっても、高品質なコードを維持しようとする態度を支えます。
地道な作業の必要性
著者は、プログラミングの仕事には避けられない退屈な作業があることを指摘しています。これらの作業は魅力的ではなく、多くの開発者が積極的に取り組みたがらないものです。しかし、著者はこれらの作業を避けることの危険性を強調しています。
大規模なリファクタリングプロジェクトでは、コードベース全体にわたる変更が必要で、その多くが単純で退屈な作業となることがあります。チームの中には、この作業を後回しにしたがる人もいますが、結果的にそれが技術的負債となり、プロジェクトの後半で大きな問題となる可能性があります。
著者の指摘する「地道な作業を避けない」という原則は、特にレガシーシステムの保守や大規模なアーキテクチャ変更において重要です。例えば、古い認証システムから新しいOAuth2.0ベースのシステムへの移行を行う際、数百のAPIエンドポイントを一つずつ更新していく必要があるかもしれません。この作業は単調で退屈ですが、避けて通ることはできません。
新しい引数の追加
著者は、関数に新しい引数を追加する場合の例を挙げています。この状況では、既存のコードベース全体を更新する必要がありますが、多くの開発者はこの作業を避けたがります。著者は、デフォルト引数やオーバーロードを使用して作業を回避することの危険性を指摘しています。
Rustの場合、デフォルト引数やオーバーロードがサポートされていないため、関数のシグネチャを変更する際は特に注意が必要です。例えば:
// 変更前 fn find_nearby_characters(point: Point, max_distance: f64) -> Vec<Character> { // 実装 Vec::new() } // 変更後 fn find_nearby_characters( point: Point, max_distance: f64, exclude_characters: &[Character], ) -> Vec<Character> { // 実装 Vec::new() }
この変更は単純ですが、大規模なコードベースでは膨大な時間がかかる可能性があります。Rustではコンパイラがすべての呼び出し箇所でエラーを報告してくれるため、変更漏れは防げますが、手作業での修正は避けられません。しかし、著者の指摘通り、この作業を避けることは長期的には問題を引き起こす可能性が高いです。
バグの修正と波及効果
著者は、一つのバグを修正した際に、同様のバグが他の箇所にも存在する可能性を指摘しています。これは重要な指摘で、セキュリティ問題などで特に注意が必要です。
例えば、データベースクエリのSQLインジェクション脆弱性を発見した場合、同様の脆弱性が他の箇所にも存在する可能性を考え、コードベース全体を調査する必要があります。この調査と修正作業は退屈で時間がかかりますが、セキュリティ上重要です。
自動化の誘惑
著者は、退屈な作業に直面したときに、多くのプログラマーが自動化を試みる傾向があることを指摘しています。自動化は確かに強力ですが、それが本当に必要かどうかを冷静に判断することが重要です。
例えば、コードフォーマットの問題に直面したとき、すぐにカスタムツールの開発に飛びつくのではなく、まず既存のツール(Goならgofmtやgoimports)を活用することを検討すべきです。
ファイルサイズの管理
著者は、ソースファイルが時間とともに大きくなっていく問題に言及しています。これは多くの開発者が経験する問題で、巨大なファイルはコードの理解を難しくします。
Rustの場合、モジュールシステムとクレートを活用した分割が効果的な解決策となります。例えば:
// src/main.rs mod user; mod order; fn main() { let user_service = user::Service::new(); let order_service = order::Service::new(); // メイン処理 } // src/user.rs pub struct Service { // ユーザー関連のフィールド } impl Service { pub fn new() -> Self { Self {} } } // src/order.rs pub struct Service { // 注文関連のフィールド } impl Service { pub fn new() -> Self { Self {} } }
Rustのモジュールシステムはファイルシステムと密接に連携しており、ディレクトリ構造がそのまま名前空間になります。このアプローチは、コードの管理を容易にし、チームの生産性を向上させます。
まとめ
「Sometimes You Just Need to Hammer the Nails」の原則は、ソフトウェア開発における地道な作業の重要性を強調しています。この原則は、特に大規模で長期的なプロジェクトにおいて重要です。
プログラマーの三大美徳(怠慢、短気、傲慢)と組み合わせて考えると、この原則の重要性がより明確になります。地道な作業を避けないことは、長期的には効率を向上させ(怠慢)、将来の問題を予防し(短気)、高品質なコードを維持する(傲慢)ことにつながります。
著者の提案するアプローチは、現代の開発環境、特にアジャイル開発やデブオプスの文脈で重要です。継続的インテグレーションや継続的デリバリーの実践において、小さな改善や修正を積み重ねることの重要性は増しています。
しかし、ただ単に退屈な作業をこなすだけでは不十分です。重要なのは、これらの作業がプロジェクト全体にどのような影響を与えるかを理解し、戦略的に取り組むことです。例えば、レガシーコードの段階的な改善や、技術的負債の計画的な返済などが考えられます。
この原則は特にチーム全体の文化と密接に関連しています。「退屈な作業も重要だ」という認識をチーム全体で共有し、それを評価する文化を築くことが、長期的には大きな差を生みます。
例えば、週に1日を「技術的負債の返済日」として設定し、チーム全体でリファクタリングや文書化、テストカバレッジの向上などに取り組むことで、長期的にはコードの品質向上と開発速度の維持につながります。
短期的な不快感と長期的な利益のバランスを取る。面倒な作業を今やるか、後で10倍の面倒を背負うか。答えは明白だ。
おわりに
21のルールの矛盾と、その先にあるもの
本書を読み終えて、ひとつ確信したことがある。21のルールは、互いに矛盾している。
- Rule 1「シンプルに」とRule 17「大きな問題を解け」は矛盾する。シンプルさを追求すれば問題を小さく分割すべきだが、大きな問題を解く方が簡単な場合もある。
- Rule 4「3つの例を待て」とRule 21「地道に作業せよ」は矛盾する。3つ目を待つ間に、重複コードが山のように積み上がる。
- Rule 5「最適化するな」とRule 20「計算せよ」は矛盾する。計算した結果、最初から最適化が必要だと分かることもある。
これは本書の欠陥ではない。プログラミングという行為の本質だ。
どのルールを優先するかは、コンテキストによって変わる。スタートアップと大企業で違う。新規開発と保守で違う。チームの習熟度で違う。言語によっても違う。
著者が25年間同じコードベースを触り続けたからこそ見えた真実がある。それは「正しいルール」など存在しないということだ。存在するのは「この状況で、このルールは適用可能か」という判断だけだ。
本書の限界
本書にも限界がある。正直に書いておく。
ゲーム開発という特殊なドメイン:著者の経験はゲーム開発に偏っている。Webサービス、インフラ、データパイプライン、組み込みシステム——それぞれのドメインで、ルールの重みは変わる。「パフォーマンスより可読性」というRule 5の前提は、リアルタイムシステムでは成り立たない。
25年という時間の二面性:長期プロジェクトの知見は貴重だが、現代の開発サイクルは短くなっている。「25年後も使うコード」を前提にした設計原則は、「6ヶ月でピボットする」スタートアップには過剰かもしれない。
単一チームの視点:著者のチームは一貫して高いスキルレベルを維持していたようだ。しかし現実には、スキルレベルの異なるメンバーが混在し、入れ替わりも激しい。「チーム全員が同じ判断ができる」という前提は、多くの現場では成り立たない。
それでも、この本を読む価値
批判を書いた上で言う。この本は読む価値がある。
なぜか。著者が自分の限界を知っているからだ。「これは私たちのやり方であり、あなたの現場では違うかもしれない」と繰り返す謙虚さ。25年の経験を持ちながら、「たぶん」「おそらく」と留保をつける姿勢。
プログラミングのルール本は多い。しかし、「ルールを疑え」と言うルール本は少ない。本書の真の価値は、21のルールそのものではなく、「ルールとどう向き合うか」という姿勢を示していることにある。
読者への問いかけ
この記事を読み終えたあなたに、いくつかの問いを投げかけたい。
- あなたの現場で「死んでいる」ルールは何か? そのルールが死んだ理由は、技術的か、組織的か、政治的か?
- あなたが無意識に従っているルールは何か? そのルールは、本当にあなたの現場に適合しているか?
- あなたが「正しい」と確信しているルールは何か? そのルールが通用しない状況を、想像できるか?
本書を読むだけでは、何も変わらない。本書のルールを、あなたの現場で検証し、適用し、時には否定すること。それが「ルールは現場で死にました」というタイトルに込めた意味だ。
ルールは死ぬ。しかし、ルールと向き合った経験は残る。その経験の蓄積が、判断力を形成する。
ちなみに、この記事を書く過程でもルールは何度か死んだ。Rule 1「シンプルに」は1万字を超えた時点で形骸化した。Rule 4「3つの例を待て」は無視して、思いついた例をどんどん書いた。Rule 21「地道に作業せよ」は、締め切りが近づいて「えいや」で公開した時点で死んだ。
ルールについて書く記事が、そのルールに従えていない。これもまた、本書の正しさを証明しているのかもしれない。あるいは、私の能力不足を証明しているだけかもしれない。たぶん後者だ。
みなさん、最後まで読んでくれて本当にありがとうございます。途中で挫折せずに付き合ってくれたことに感謝しています。
読者になってくれたら更に感謝です。Xまでフォロワーしてくれたら泣いているかもしれません。