長期的な(long term)ソフトウェア開発
(berthub.eu)- 現代のソフトウェアは継続的デプロイ(CD)と自動化テスト(CI)によって頻繁に更新されるが、「長期的に使用されるソフトウェア」には別のアプローチが必要
- 例: 原子力発電所、航空機、ペースメーカー、選挙システムなど
- 信頼性と安定性が重要な分野では、継続的な変化よりも安定性と予測可能な変更が好まれる
- 例: 原子力発電所、航空機、ペースメーカー、選挙システムなど
長期ソフトウェア開発の中核原則
依存関係(Dependencies)
- ソフトウェアの依存関係は長期的な成功における重要な要素
- ソフトウェアは外部世界との相互作用を考慮する必要があり、プログラミング言語のような基本的な選択が重要
- ソフトウェア依存関係の階層を理解する
- 外部世界: 私たちが制御できないクライアントソフトウェア(例: ブラウザなど)。
- 基本的な選択: プログラミング言語のように、変更するにはスタック全体を書き直す必要がある要素。
- フレームワーク: Spring Framework、React など、コードベースと強く結びつくもの。変更は可能だが、非常に高いコストがかかる。
- データベース: ほとんどは置き換え可能だが、細かな調整と作業が必要。
- ヘルパーライブラリ: 特定の機能を提供する置き換え可能なライブラリ。
- 時間の経過とともに依存関係と外部世界は変化する:
- 依存関係の変化により、コード修正または 振る舞いの変化 が生じる可能性。
- 新しいメジャーバージョンのリリースにより 互換性の問題 が発生。
- プロジェクトが 中断 されたり消滅したりするリスク。
- セキュリティリスク: 依存関係が悪意ある行為者によって侵害される可能性(npm、PyPI など)。
- 商用化: ベンチャーキャピタル(VC)の新しいオーナーが有料化する。
- 依存関係間の 衝突 問題。
- 長期利用を考慮して依存関係を選ぶ際に確認すべき項目:
- 技術レベル: ソースコードを見て品質を判断できるか。
- ユーザーベース: 誰が使っているかを確認する。
- 開発目的: 開発者が誰で、目標が何かを把握する。
- 資金支援: 資金提供の有無とその出所。
- メンテナンス: セキュリティリリースが定期的に行われているか確認する。
- コミュニティがメンテナンスを引き継ぐ可能性。
- 自分でメンテナンスできるか。
- 必要なら資金支援によってプロジェクトの継続性を確保すべきか。
- 依存関係の依存関係:
- 下位依存関係のセキュリティ履歴も確認する。
- 現実的なアプローチ
- 依存関係を制限する:
- 1600個を超える依存関係を持つプロジェクトは、コードが急激に変化し不安定になりやすい。
- 膨大な依存関係を持つプロジェクトでは、どんなコードをデプロイしているのか把握することすら難しい。
- 慎重に追加する:
- 依存関係を追加するときは 技術的な難易度 を与え、自然なレビュー時間を確保する。
- 長期プロジェクトでは不要な依存関係は避けるべき。
- 依存関係を制限する:
ランタイム依存関係(Runtime Dependencies)
- これまで議論してきた内容は ビルド/コンパイル依存関係 に限られている。
- しかし現代のプロジェクトはしばしば ランタイム依存関係 も含む:
- 例: Amazon S3、Google Firebase。
- 一部は事実上の標準とみなされている(S3 など)。
- しかし大半のランタイム依存関係は、特定のサービスへの ロックイン(Lock-in) の性質が強い。
- 10年後 に現在利用しているサービスの代替手段を見つけるのは非常に高コスト。
- サードパーティサービス依存の一覧は最小化、あるいは空にする 必要がある:
- とくに クラウドネイティブ(cloud native) なソフトウェア開発では、高度なサードパーティサービスを多数使うのが一般的。
- 長期プロジェクトではこうした依存関係は高いリスクを伴う。
- ビルド時サービス依存 も重要な要素:
- 例:
npm installがもはや動作しなくなれば、ソフトウェア自体をビルドできない。 - これはプロジェクトの 再利用可能性 を深刻に低下させうる。
- 例:
- ランタイム依存関係を徹底的に検討する:
- 潜在的なロックイン問題を認識し、依存関係を減らすか排除する。
- 長期的な維持可能性を確保する:
- クラウドやサードパーティサービスを置き換えられる可能性を事前に考慮する。
テスト、テスト、そしてテスト
- テストの必要性は 誰もが同意する基本原則:
- 可能な限り 多くのテスト を書く。
- すべてのテストが等しく価値を持つわけではないが、テストそのものを後悔することはほとんどない。
- とくに 依存関係の多いプロジェクト ではテストが必須:
- 依存関係が変更されたりドリフトしたりした場合、問題を早期に検知する助けになる。
- テストの役割
- 問題解決を支援:
- 変更状況に合わせて素早く調整できる。
- リファクタリングを支援:
- コード依存関係を取り除いたり変更したりするときに自信を与える。
- 長期保守に有用:
- 開発が 3年以上中断された後 でも、テストによってシステムがまだ動作するか確認できる。
- 新しいコンパイラ、ランタイム、OS でも機能が維持されるか確認できる。
- 問題解決を支援:
- テストはコストではなく投資
- より多くのテストを書く:
- テストは保守性と安定性の基盤。
- コードを修正または拡張するとき、テストは大きな心理的支えになる。
- より多くのテストを書く:
複雑さ: ソフトウェア開発の最終ボス
- 複雑さはソフトウェア開発の究極の敵:
- 最高の開発者やチームであっても、複雑さによって崩されうる。
- エントロピーと人間の行動の影響で、複雑さは常に増大する。
- 複雑さを意識的に管理しなければ、プロジェクトは 保守不能な状態 に陥りうる。
- 複雑さとコード量の相関関係
- コード量と複雑さ:
- コードが少ないうちは多少複雑でも管理可能。
- コードが増えるほど、制御可能であるためには単純さを維持しなければならない。
- 管理可能な複雑さはチームの能力と「緑の三角形」の中に収まっている必要がある。
- 複雑さの限界:
- チーム人数を増やしたり卓越した能力の開発者を採用したりしても、複雑さ処理には限界がある。
- その限界を超えると、プロジェクトは保守不能な状態に陥る。
- コード量と複雑さ:
- コードが常に「右上」へ動いていく理由:(グラフ上で)
- さらに多くの機能要求。
- 不要な最適化の試み。
- バグ修正時に既存の複雑さを減らす代わりに新たなコードを追加する。
- 誤った API 設計のコスト:
- 例:
CreateFile関数は大半の場合ファイルを作成しない。 - こうした混乱は 追加の認知負荷 とミスの可能性を高める。
- 例:
- 複雑さを管理する戦略
- リファクタリングは早く、そして頻繁に:
- 不要なコードを削除し、単純化に時間を投資する。
- テストに投資する:
- テストが多いほど、複雑さを減らす作業は容易になる。
- 複雑さ管理の重要性:
- 単純化のために 事前に努力しなければ、長期プロジェクトは最終的に「保守不能状態」に陥るリスクがある。
- リファクタリングは早く、そして頻繁に:
退屈で単純なコードを書け。もっと単純に。そしてもっと退屈に。
"デバッグはプログラムを書くことより2倍難しい。したがって、コードを書くときに可能な限り賢くしてしまったら、それをどうやってデバッグするのか?" - Brian Kernighan
- とにかく退屈で明快なコードを書く:
- ナイーブ(näive)でも 直感的に理解できるコード を好む。
- 「プレミアムな最適化はあらゆる悪の根源だ。」
- 最適化は本当に必要なときだけ:
- あまりに単純で問題になるなら、後から複雑さを加えるのは難しくない。
- その瞬間が来ないかもしれない。
- 複雑なコードを書くのを避ける:
- 本当に必要な時点まで待つこと。
- 単純なコードを書いたことを後悔する可能性は非常に低い。
- 高性能コードや機能は特定の環境でしか動作しないことがある。
- 例:
- LMDB: PowerDNS で安定して使えるようになるまで多くの困難があった。
- RapidJSON: SIMD アクセラレーション JSON ライブラリ。性能は高いが利用条件が厳しい。
- 例:
- 「自分ならこの制約を克服できる」と自信があっても:
- 今年は可能でも、5年後の自分や後任の開発者は苦労するかもしれない。
- 複雑なプログラミング言語 にも同じ原則が当てはまる。
- 結論:
- コードを単純化せよ:
- 本当に単純に。もっと単純に。
- 最適化は後回しにせよ:
- 複雑さは必要なときに追加できるが、初期から複雑にすると保守が難しくなる。
- コードを単純化せよ:
LinkedIn ベースのソフトウェア開発
- 現実 vs. 理想
- 理想的なアプローチ: 依存関係を選ぶ際には、徹底した評価と検討が必要(上記のチェックリストを活用)。
- 現実的なアプローチ: ときには魅力的な技術を試し、動けばそのまま使ってしまう傾向がある。
- 魅力的に見える理由
- LinkedIn の著名人やインフルエンサーが勧めた技術。
- Hacker News のようなコミュニティで絶賛される「最新フレームワーク」。
- 流行技術は長期的な検証が不足している:
- 「10年以上維持されるソフトウェアプロジェクト」には向かないかもしれない。
- 新しい技術は初期段階で安定性や保守性の面で問題が起きやすい。
- 推奨事項
- 実験的な領域でのみ使う:
- 新技術はまず小さなプロジェクトや非中核領域で試す。
- Lindy 効果 を考慮する:
- 技術の寿命は現在まで使われてきた期間に比例する傾向がある。
- 古い技術ほど長期的な安定性を期待できる。
- 実験的な領域でのみ使う:
- 新技術は魅力的だが、長期プロジェクトには実証済みで安定した技術の方が適している。
ロギング、テレメトリ、性能
- ソフトウェアが継続的に更新・デプロイされない場合:
- Web サイトが壊れたときに 即時フィードバック を得られない可能性が高い。
- デプロイ後、実際の問題解決まで長い時間がかかることがある。
- 初回リリースから徹底したロギングとテレメトリを実装する:
- ソフトウェアの 性能、失敗、活動履歴 を記録する。
- 時間とともに蓄積されたデータは、まれに発生するバグの解決に非常に有用。
- 不十分なロギングによる問題:
- ユーザー向け UI をデプロイしたところ、3000個のフォルダ を作成したユーザーが問題を報告。
- ユーザーは「動かない」としか言わず、根本原因の把握に数か月かかった。
- 性能ロギングとテレメトリ があれば、もっと素早く問題を解決できたはず。
- ロギングとテレメトリは必須:
- ソフトウェアの活動を徹底的に監視できるよう設計する。
- 長期的なデプロイと保守の過程で 予期しない問題の解決に大きく役立つ。
ドキュメント化
- ドキュメント化の重要性:
- 単に API ドキュメントを丁寧に書くことにとどまらず、「なぜこの設計にしたのか」 を説明しなければならない。
- システムがどう動くかに関するアイデアや哲学を記録する。
- 解決策を分離した理由や、直感に反する設計判断の根拠 を残すべき。
- アーキテクチャ文書以外に有用な資料:
- 社内ブログ記事: 開発者がシステム設計について自由な議論を共有する。
- チームインタビュー: 設計判断の背景を含む会話記録。
- こうした文書は、時間がたってもチーム内で知識継承を可能にする。
- コードにコメントを残せ:
- 「良いコードにコメントは不要」という風潮にもかかわらず、コードの『なぜ』を説明するコメントは不可欠。
- 特定の関数が存在する理由を説明する内容が重要。
- コミットメッセージを書く:
- コミットメッセージは作業記録の中核であり、コード変更の理由を追跡できるようにする。
- ユーザーが簡単にコミットメッセージを閲覧できる環境を整える。
- ドキュメント化の時間を確保する:
- 開発がうまく進まない日は、有用なコメントや記録を残すことに時間を使う。
- チームとしてドキュメント化のための時間を定期的に割り当てる。
- なぜそのように設計したのかを記録せよ:
- 7年後に新しいチームへ哲学と背景を伝えられる資料は何よりも貴重。
- コメントとコミットメッセージで履歴を残せ:
- 開発中だけでなく、長期的な保守のためにも不可欠な要素。
チーム構成
- チームの継続性とソフトウェアの長期的成功:
- 一部のソフトウェアは80年のサポートを想定して設計される。このような長期プロジェクトではチーム維持が中核となる。
- 現代の開発環境では、平均3年程度でも長い在籍期間とみなされる。
- 優れたドキュメントとテストはチーム交代をある程度補えるが、限界がある。
- 長期在籍の利点:
- 10年以上チームメンバーを維持する:
- 実際の従業員として雇用し、開発者を適切にマネジメントすることが重要。
- 長期プロジェクト成功のための重要な「ハック」とみなせる。
- 10年以上チームメンバーを維持する:
- 外注依存の問題点:
- 外注開発者はコードをシステムに引き渡した後で去ることが多い。
- 10年以上持続可能なソフトウェア品質を目指すなら、非常に非効率な方法。
- チームメンバーが長期的に一緒に働ける環境を整える。
- 外部コンサルタントへの依存を最小化し、社内チームの持続可能性を高める戦略が必要。
オープンソースを検討せよ
- オープンソースの利点:
- 外部レビューを通じてコード品質を維持できる:
- 外部の目 が開発者により高い基準を求める。
- より良いコード標準を維持するための強力なメカニズム。
- 外部レビューを通じてコード品質を維持できる:
- オープンソース化準備の現実:
- 企業や政府はしばしば、オープンソース化の準備に数か月から数年かかると主張する。
- 理由:
- 内部では外部公開するには気が引けるコードが一般的に書かれている。
- オープンソース化前にコード整理が必要だから。
- 適用可能性を評価する:
- オープンソースが常に可能な選択肢とは限らない。
- 可能であれば、コード品質と透明性を高める良い方法。
- オープンソースは可能なときに活用すべき重要な戦略。
- 外部の目と高い基準は、プロジェクトを正しい方向に保つ助けになる。
依存関係の健全性チェック
- 依存関係の変化がもたらす問題:
- 依存関係は時間の経過とともに期待と異なる形で 変化 または 逸脱 することがある。
- これを放置すると:
- バグ の発生
- ビルド失敗
- その他の残念な結果につながりうる。
- 定期的な健全性チェックを推奨:
- 定期的な依存関係点検:
- 問題を事前に見つける機会を与える。
- 依存関係の新機能を発見し、コード簡素化や他の依存関係の除去の可能性も探れる。
- 予防保守 の重要性:
- 自分で点検時間を計画しなければ、結局問題が起きたときに強制的に時間を取られる。
- 定期的な依存関係点検:
- 保守の比喩:
- 機械工たちの格言:
- 「保守の時間は自分で計画せよ。そうしなければ、その時間は機械が計画する。」
- 機械工たちの格言:
- 定期的な依存関係点検は、長期的なソフトウェアの安定性と効率性のために不可欠な活動。
- 問題を前もって解決し、前向きな変化を見つける機会として活用する。
主な参考書籍
- The Practice of Programming (Brian W. Kernighan, Rob Pike)
- The Mythical Man-Month (Fred Brooks)
- A Philosophy of Software Design (John Ousterhout)
- Kill It with Fire: Manage Aging Computer Systems (Marianne Bellotti)
最後に
長期的なソフトウェア開発のための中核的な推奨事項:
- 単純さを保つ:
- 単純に、さらにもっと単純に! 必要なときに複雑さは追加できるので、最初から過度に複雑にしないこと。
- 単純さを保つには 定期的なリファクタリングとコード削除 が必要。
- 依存関係を慎重に考える:
- 依存関係は少ないほどよい。綿密に検討 し、監査する。
- 1600個の依存関係を監査できないなら、計画を見直すべき。
- トレンドや流行(例: LinkedIn ベース開発)に流される選択は避けること。
- 定期的な依存関係点検: 依存関係の状態を継続的に監視する。
- テスト、テスト、そしてテスト:
- 変化する依存関係を適時に把握する。
- リファクタリング時に自信を与え、単純さを保つ助けになる。
- ドキュメント化:
- コードだけでなく、哲学、アイデア、「なぜこうしたのか」という背景まで文書化する。
- 将来のチームメンバーにとって貴重な資産になる。
- 安定したチームを維持する:
- 長期プロジェクトへの投資として 長期雇用 を検討する。
- チームメンバーが長期間プロジェクトにコミットできるよう支援する。
- オープンソース を検討する:
- 可能であれば、オープンソースを通じてより高いコード標準を維持する。
- ログと性能テレメトリ:
- 問題を早期に把握して解決するうえで重要な役割を果たす。
- これらの推奨事項は新しいものではないかもしれないが、経験豊富な開発者たちが強調するだけに、深く考える価値がある。
4件のコメント
安定性が重要なレイヤーと速度が重要なレイヤーを分け、その両者の関係をどう扱うかが最も重要なエンジニアリングの力です。
Tossが安定性だけを追求していたなら、他の銀行と変わらなかったでしょう。
危険なのは、SpaceXもそうですし、テスラもそうですし……
履歴書主導の開発が問題なのでしょうか。
Hacker Newsの意見
ツールチェーンを積極的に更新することは、開発プロセスの重要な一部である。多くの企業はツールチェーンのアップグレードを優先順位から外しており、その結果、セキュリティ脆弱性のような問題を招いている。最新のコンパイラやビルドシステムのリリースごとにブランチを作成してビルド状態を確認し、エラーがあればバグと見なして即座に対処する。これは、コードベースを最新の言語機能で段階的にモダナイズし、リファクタリングする助けになる。
サードパーティ依存関係は、長期的には期待外れになることが多い。新しいプロジェクトではサードパーティ依存関係を使って短期的に問題を解決できるが、長期的には自前のコードに置き換えるほうがよい。
依存関係をベンダリングし、コードレビューを通じて管理することが必要である。サードパーティコードの品質は低いことも多く、自分で書いたほうが良い場合がしばしばある。
Qt、CMake、モダンC++を使って、長期的な拡張性を目指すプロジェクトを進めている。こうした技術スタックは、継続的に機能や改善を提供している。
Emacs Lispで作業するのは新鮮な経験だった。ライブラリが更新されなくても安定して動作するのが利点である。GatsbyとNodeを使った経験では、アップデートの問題によって苦労した。
シンプルなコードを書くことが重要である。複雑なコードは必要なときにだけ書くべきであり、シンプルなコードを後悔することはない。
システムとコードのドキュメント化は重要である。ソフトウェア開発の経験を積むほど、ドキュメント化の重要性を実感するようになる。
テストは計画において重要な役割を果たす。NASAの開発手法を参考にして、プログラミングエラーを見つけることに注力すべきである。医療ソフトウェア開発では、解釈の余地を避け、動的メモリ割り当てを使わない。
長く使われるソフトウェアを書く最良の方法は、「退屈な」コードを書くことである。依存関係を避け、基本に忠実であるべきだ。
Pythonで依存関係の問題に苦しんだ経験がある。これは「DLL Hell」と呼ばれ、COMがこれを解決しようとしたが、うまくはいかなかった。
産業用ソフトウェアに適用される慣行は、一般的なソフトウェアに適用するには十分に堅牢ではない。エンジニアはリスクを軽減しようとしているが、私たちはリスク軽減に重点を置いている。