冷血型ソフトウェア
(dubroy.com)- 一部のソフトウェアプロジェクトは、継続的な開発活動があってこそ維持される温血型であり、活動が途切れても再び続けられるなら冷血型に近い
- 冷血型プロジェクトは、長く停止していてもビルドとテストが壊れないように退屈な技術を選び、変わったり消えたりする可能性のある外部サービスへの依存を避ける
- 依存サービスの買収・廃業、コンパイラのアップグレード、パッケージのサポート終了は、活動の少ないプロジェクトを再開するときに保守コストとして返ってくる
- 個人プロジェクトのように1年、2年、3年と手を入れないコードは、継続的な熱を生み出しにくいため、最初から低い変化率を前提に設計すべき
- ブログ用の静的サイトジェネレーターは、2012年の最初のコミット以降、Python 2、リポジトリに含まれる4つのサードパーティモジュール、ローカル実行、
ssh越しのrsyncによるデプロイだけで、ほぼ修正なしに動き続けている
冷血動物の比喩で見るプロジェクトの維持方法
- 2004年の自然史の講義で、教授は冷凍庫から取り出したニシキガメの子ガメをカメラの下に置いて講義を進めた
- ニシキガメの子ガメは、凍った状態でも生き残れる数少ない種の一つだった
- 1時間のあいだ、カメはほとんど見えない動きから始まり、最後には画面の半分ほどを移動した
- 温血動物は狭い範囲の体温を維持しなければならず、人間は約37°C付近から外れると問題が生じる
- 冷血動物は周囲の温度に合わせて代謝を調整し、暖かいときは活発で、体と環境が冷えるほど動きが遅くなる
- ソフトウェアプロジェクトも同じように分けられる
- 温血型ソフトウェアは、プロジェクトに継続的な動きと熱があるときにうまく回る
- 6か月間止めておくと、再び取り出したときには死んだプロジェクトのようになっていることがある
冷血型ソフトウェアの条件と例
- 温血型プロジェクトが再開しにくくなる理由は、外部の変化が蓄積されるためである
- CIが依存するサービスが買収されたり資金が尽きたりして、動作しなくなることがある
- 新しい依存関係を追加しようとしてコンパイラのアップグレードが必要になることがある
- 別のパッケージはサポートが終了し、最新のコンパイラでは動作しないことがある
- 一人で作業し、ひらめいたときだけ変更して1年以上再び手を入れないプロジェクトは、温血型として運用するのが難しい
- 冷血型プロジェクトは、凍りついたニシキガメの子ガメのように、1年後でも止まった地点から再開できなければならない
- そのためにboring technologyを使い、ビルド・テストスクリプトが、変わったり壊れたり完全に消えたりする可能性のある外部サービスに依存しないようにする
- 依存関係は vendored dependencies のように、プロジェクトのリポジトリ内に含める
- このブログを動かしているソフトウェアが、冷血型プロジェクトの例である
- 最初のコミットは2012年1月8日で、古いWordPressインストールを置き換えるための小さな静的サイトジェネレーターだった
- Python 2で書かれており、4つのサードパーティモジュールに依存し、そのすべてがプロジェクトのリポジトリにコミットされている
- すべての処理はローカルで実行され、成果物は
ssh越しのrsyncでデプロイされる - いくつかの小さな改善を除けば、修正なしに動き続けており、今後12年後も動き続けることが期待される
1件のコメント
Hacker News の意見
Node と JavaScript エコシステムの Web フレームワーク Express は、現在のメジャーバージョン 4.x.x が 10年以上維持されている https://www.npmjs.com/package/express?activeTab=versions
それでも毎週1,700万回以上ダウンロードされるほど広く使われており https://www.npmjs.com/package/express、機能が不足していたり性能が最高ではなかったりはするが https://fastify.dev/benchmarks/、高速で安定した開発と長期計画が可能な点がよい
古いバージョンのセキュリティパッチ停止や急激な API 変更を心配しなくてよく、Go は広範な標準ライブラリと互換性の約束のおかげで、10年以上前のプログラムも実行できるため、さらに安定している https://go.dev/doc/go1compat
最初に登場したときは、JavaScript で書かれているというだけで、未熟な偽物プログラマー向けのゴミ扱いをよくされていたが、その後いくつもの会社で Express を使って実際にお金を稼ぐ素晴らしいサービスを作る手助けをしてきたし、今では膨大なリクエストを処理しているはずだ
最近は Go でも多く書いているが、それでも Express でサービスを作ることには十分満足しており、全体として良いソフトウェアだと思う
実際に嫌いというより、絶え間ないバージョンアップの堂々巡りがあるので選ばないと思う
Python は冷血ソフトウェアの例としてはかなり悪い
ランタイムとツールの両方で 破壊的変更 が続いており、筆者もすでにかなり前にサポート終了した Python 2 を使い続けなければならない状況にある
より良い例は Go や Java のように、10年前のコードが現代のツールでもよく動く言語であり、さらに極端な例としては Perl のように、30年前のコードもまだよく動くものだ
ソフトウェアを作っていると、ユーザーが意図しない方法で何かをできるようにしてしまうミスをすることがあるが、Java の世界では、より新しく安全で意図が明確な機能を追加し、ユーザーに移行を促す形で解決する
Python も似ているが、そこに「そして近いうちに古い機能をオフにする」が付いてくる一方、Java はそうしない
例えば
java.net.URLのequalsメソッドは壊れた設計として知られており、強く非推奨とされているが、20年以上にわたりサポートされ続けているPython Airflow の空のオペレーターは、しばらく
DummyOperatorという名前をサポートしていたが、「dummy」が歴史的・文化的に蔑称として使われたことがあるという理由で、メンテナーがEmptyOperatorを使うよう変更し、既存の名前を壊したアップグレードすると、参照名を変更するまでコードのロード時点でエラーになり、個人的にはこのような形でユーザーを壊すことはしないと思う
Java の世界なら、名前変更程度はテキスト置換で十分対応できるので、維持できない理由が出てくるまでサポートし続けただろう
そのため全体として、Java と Java ライブラリの依存関係 は Python よりはるかに自由にアップグレードできると思う
Java LTS バージョンを使い、良い依存関係を選べば、いつでも再実行できるようにできる
Python では、機械学習の授業で、ある依存関係が一晩で壊れる API 変更を入れ、講師は授業準備を始めた数週間前の最新バージョンをそのまま使っていたため気づかなかった、ということがあった
Python 2 から 3 への移行時は破壊的変更だったが、それは一度きりの変化であって、「継続的な破壊的変更」ではなかった
同じメジャーバージョンに留まるなら、新しいマイナーバージョンで古いコードが壊れることはなく、例えば古い 2.x コードは 2.7 でよく動き、古い 3.x コードは 3.12 でもよく動く
マイナーバージョン変更で新機能が追加されることはあるが、古い 3.x コードが
asyncキーワードや型ヒントを使っていないからといってコードが壊れるわけではないできれば Python を避ける理由の一つがこれだ
今日書いた Python コード が数年後にも動作する可能性は低いと感じており、かなり大きな問題だと思う
新しい SDK で 3年前の Java コード を実行しようとしても、いつもどこかが壊れていた
IBMメインフレーム(z/OS)で仕事をしているが、後方互換性の維持に関しては、IBMほど徹底しているところはほとんど見たことがない。
Microsoft Windowsが2位、LinuxカーネルABIが3位くらいだと思うが、Linuxエコシステム全体で見るとごく一部にすぎない。
それ以外の大半はchurnに近く、オープンソースでは趣味で後方互換性に時間を使いたがる人は少ないように見える。
経済的には囚人のジレンマのように見える。誰もが互換性維持のコストを他人に押し付け、その結果として全員にとって役に立たない仕事をさらに増やしている。
たとえばレトロコンピューティングコミュニティを見ると、新しいハードウェアが古いOSで動くようにドライバを書くことも珍しくない。
報酬がないなら、結局は自分が作っているプラットフォームをどれだけ大事に思っているか次第で、私はABI安定性に対する実証済みの献身を理由に、Linuxカーネルをシステムコールで直接ターゲットにすることにした。
一方で、自作のプログラミング言語はできるだけ「完璧」にしたいので、ずっと直し続けたい気持ちになる。
もしかすると誰かが猛烈に使おうとするかもしれないので、READMEにはまだ初期開発段階で不安定だという告知を入れてある。
RubyやPythonを作っている人たちも似たように感じていると思うし、言語が子どものように感じられて成功してほしいから、
printがキーワードだったような間違いは直すべきだと考えるのかもしれない。むしろ後方互換性のための部分が壊れることも多い。
以前の職場ではコンテナ化されたNodeアプリを作っていて、CIがNodeのソースからイメージをビルドしていたが、しばらく触っていなかったサービスのデプロイが突然失敗し始めた。
調べてみると、Dockerfileがサポート期間の終わったUbuntuイメージをベースにしていて、更新リポジトリがアーカイブリポジトリに移動したため、Dockerfileを修正しないとイメージをビルドできなくなっていた。
触っていないソフトウェアが壊れる例であり、だから私はGoと単一バイナリを好む。
リリースとしてパッケージ化しておけば再ビルドする必要もなく、Distroless Dockerイメージには自分のバイナリ以外の依存関係がない。
Goを長く使っているが、ソフトウェアが年を取って壊れる問題に遭遇したことはなく、NodeやPHPを使っていたときに感じていたさまざまな問題が消えた。
Node側で2番目に大きな問題はフレームワークの間接化パターンで、1番目はパッケージ管理だ。
「Xバージョンをインストールしたが、YモジュールにはZバージョンが必要だ」といったpeer dependencyの問題がずっと出てくる。
多くのエンジニアはGitHubでライブラリを探すとき、最後のコミット日時を確認する。
最近コミットされているほど、よりよくサポートされているライブラリだと考える傾向がある。
しかし、必要なことを正確にこなし、バグが0個で、何年も安定していたアーカイブ済みプロジェクトなら、古道具屋で隠れた名品を見つけたようなものだ。
最近の多くのエンジニアは「継続的に」更新されていないライブラリを自動的に捨てるが、それを良いことのように考えているように見える。
現代のソフトウェア開発環境はそうでないことが多く、Webフロントエンドは頻繁に変わる代表的な例だ。
完全に独立したライブラリなら更新がなくても問題ないかもしれないが、Webフロントエンドフレームワークに依存するライブラリは、エコシステムの変化に合わせて更新されなければ問題を起こす。
実際の数値は知らないが、圧倒的多数の場合、最近の活動がないことは「完成していてバグがない」ではなく「放棄された」ことを意味すると思う。
ある言語は1.0バージョンとはほとんど別物になり、ある言語は書かれたコードの大半を維持したまま上にだけ追加していた。
結局、その傾向はコミュニティやエコシステムにも反映されるようだ。
Clojureは破壊的変更をほとんどしないためリストの上位にあったと記憶しており、5年前に最後に変更されたライブラリでも現在の言語バージョンで完全に問題なく動く。
Lisp系なので、言語の中核をupstreamの変更なしに拡張できる点も助けになっているようだが、当然それ自体の欠点もある。
それでも「新しさ」が「優秀さ」と同じだという考えをやめさせてくれた点は良かった。
最近は去年新しく作られたライブラリより、数年前からほとんど変わっていないライブラリをより頻繁に使っており、大きな問題もない。
ある言語は1〜2年ごとにリリースされ、よく使われているがぎこちなかったパターンを置き換える、新しく優雅な構文や標準ライブラリの抽象データ型を追加する。
その言語コミュニティは新しい構文をほぼ即座に「慣用的」だと見なし、昔の鈍重な方法で書かれたコードは直すべきだと考える。
特定のコードベースを変えようとする理由は、たいてい新しい構文に比べて既存の方法のほうが不透明で、保守やコードレビューを難しくするというものだ。
新しい構文が最初から存在していたなら、誰も昔の方法を良いコードだとは思わなかったはずなので、新しい開発者にとっての可読性と貢献の参入障壁を下げるためにコードを更新すべきだ、という理屈だ。
こうした言語で実装されたライブラリが3年以上更新されていないなら、しばしば悪い兆候になる。
開発者が、最新の形の言語を学んだ他の開発者にも簡単に読める慣用的なコードとして保つほどコミュニティとつながっていない、という意味かもしれず、外部PRを受ける関心もないのかもしれない。
プロジェクトが放棄されたものとして表示されているため誰も報告していないだけで、セキュリティ脆弱性があるかもしれない。
更新なしで過ごせるソフトウェアは、最初からきちんと作られたソフトウェアだけだと思う
自分だけのためのソフトウェアなら比較的簡単で、10年経っても好みが大きく変わらない可能性は高いし、
nが小さければO(n)関数があってもO(n^2)関数を使うような小さな問題は無視できるしかし他人が使うソフトウェアなら要件は異なり、十分に大きな
NではO(n)関数に価値が出てくる、といった具合に問題が起きる自分のために使うにせよ他人のために使うにせよ、予測していなかった問題は出てくることがある
たとえば1GBを超えるファイルを処理するとクラッシュするが、普段は100KB未満のファイルしか使わないので気にしておらず、直そうとしたら半分を書き直さなければならないかもしれない
変わらないソフトウェアは頻繁に変わるソフトウェアより本質的に優れている、という考えに対する最大の反論はここにある
変わらないソフトウェアが最初から完璧だった可能性もあるが、深いところに恐怖が潜んでいる可能性もあり、事前に見分けるのは難しいからだ
速く更新されるソフトウェアが遅く更新されるソフトウェアより本質的に優れている、という意味でもなく、更新速度以外にも多くの要素がある
要件が変われば、当然ソフトウェアも変わるべきだ
しかし10年の間には、要件の変化とは無関係に多くのことが起こり得る
オープンソースプロジェクトが放棄されたり方向転換したり、商用ソフトウェアが終了したり、会社が買収されたり、App StoreやPlay Storeのルールが変わったり、APIが消えたり価格が変わったりして、プロジェクトの採算性が崩れることもある
ツールチェーン、フレームワーク、プログラミング言語、パラダイム、ベストプラクティスも変わる
核心は、要件と無関係な外部変化に自分が変化を強制されないようにすることだと思う
良い原則だが、いつものようにトレードオフはある
安定していることと古びてしまっていることは違い、その違いはしばしばセキュリティで分かれる
重要な新要件を満たすのは簡単だが、vendoredライブラリをメジャーバージョン7つ分上げなければならず、その結果、無関係な破損が大量に発生するならどうするのか
時が止まったツールセットに慣れた人がもはや十分におらず、誰も学ぼうとしないならどうするのか
依存関係を慎重かつ保守的に選ぶのは良いが、そうして小さく保った依存関係の変化にすら追随しないのは、一歩行き過ぎだと思う
文章の情緒には共感する
ほんの数年前に作ったモバイルアプリですら、今ではパッチを当ててアップデートを提出するのに何十時間も必要になるのが本当に嫌だ
筆者が自分の静的サイトジェネレーターを冷血ソフトウェアと呼び、Python 2で動くと言っていた最後の部分も興味深い
Python 2は最近インストールがどんどん難しくなっており、結局そのプロジェクトも温血プロジェクトになるだろう
Xcodeをアップグレードするたびに、プロジェクトがきれいにコンパイルされ動作するよう細かなことを直さなければならず、本当に腹立たしく、本来まったく受け入れられるべきではないことだ
最近のgit履歴メッセージはすべて「最新のXcodeで動作するよう修正」のバリエーションだ
こうした下位SDKやOSの変更がセキュリティ上の脅威のために必要だったならある程度は理解できるが、ほとんどの場合そうではない
たいていはAPIを廃止し、デフォルトの警告を追加し、今後はこのフレームワークではなくあのフレームワークを使え、というような馬鹿げた変更だ
プラットフォームとフレームワークは、意図的に動く標的になるのをやめるべきで、特に今や非常に安定して信頼できるOSであるならなおさらだ
10年前のプロジェクトを冷凍庫から取り出しても、10年前と同じようにきれいにコンパイルされ実行できるべきだ
これらのOSベンダーは兆ドル規模の企業なのだから、後方互換性に多くのエンジニアリング努力がかかるという言い訳は聞きたくない
個人のサイドプロジェクトをずっと維持している
12〜13年前に素のPHPで始め、後にLaravelで書き直し、2017年ごろにSymfonyでもう一度書き直した
フリーランスとしてフルタイムで働いていてエネルギーがなく、6〜18か月の間にごく小さなコミットを2〜3個しかしなかった時期もあったが、時間ができれば機能を追加し、アップグレードし、実験し、学んできた
長期的にプロジェクトを維持する方法を学ぶうえで非常に役立った
依存関係の更新、不要なものの削除、セキュリティアップデートの確認、単純化の機会探し(VagrantからDockerへ、Vue + Axios + WebpackなどからHtmxへ)といったことを身につけ、避けるべきことも学んだ
個人的には、出たばかりの依存関係、マイクロサービス、Kubernetesのような複雑なインフラを避けるようになった
最近はいくつも機能を作り、PHP 8.2とSymfony 7へ上げ、ChatGPTベースの機能も統合したので、望むなら1〜3年ほど休んでもよさそうだ
過去4〜5年でこのプロジェクトは平均的なフリーランスの1年分の収入に近い売上を上げたので、眠っている無名のサイドプロジェクトでもない
数年使わずに戻ってみると、8年前に離れたときとまったく同じひどい画像操作関数がそのまま残っていた
記事で述べられていること以外にも、本質的に安全な脅威モデルが重要だ
たとえばWebサイト全体は攻撃者やスパムボットに常に対処しなければならないので、本質的に温血に近い
一方でTiddlyWikiのような静的ページはそもそもWebに公開しないこともでき、ブラウザは非常に安定したプラットフォームなので、はるかに良い
冷血プロジェクトと温血プロジェクトへの好みの違いは、https://www.cs.utexas.edu/users/EWD/transcriptions/EWD11xx/EWD1175.html に出てくる Buxton Index と関係がありそう
近所の小さな食料品店は約0.5年、真のキリスト教徒は無限大、再選を狙う平均的な政治家は約4年、たいていの産業はそれより少し長く、四半期報告書を書かなければならない管理職はずっと短い、という具合
Buxton Index が重要なのは、互いに大きく異なる Buxton Index を持つ主体同士の緊密な協力は必ず失敗し、道徳的な非難につながるからだという
短い側は表面的で近視眼的だと非難され、長い側は職務怠慢や責任回避、ただ乗りだと非難される
互いを愚かだとも見なすようになる
Buxton Index の利点は、単純な数値概念なので道徳的に中立であり、違いを道徳論争の上に引き上げる点にある
学界と産業界の協力を考えるときには特に重要
この名前は本当にいまいち
冷血動物は環境に大きく依存し、温血動物は代謝によって外部温度への依存を減らしている
いずれにせよ、不必要に曖昧
ただ「外部依存のないソフトウェア」と言えば、長々とした説明段落をなくせる
自然から持ってきた不適切な比喩で浅い結論に飛びつくソフトウェア開発の記事も嫌いだが、その比喩対象である自然現象そのものを完全に誤解したままそうする記事はもっと嫌い
ニシキガメをはじめとするいくつかの種が凍っても生き残るのは、冷血性のためではなく、特殊な不凍タンパク質のおかげ
他のトカゲや冷血動物は、解凍されるときに自分の組織が破裂するはず
https://lobste.rs/s/hitos3/cold_blooded_software#c_mxjzwh