Prompt
あまりに大きなシステムだと、バグさえも利用され振る舞いの一つとして仕様になる。
けれど、そのバグはいつどうやって直せばいいの
それは、ソフトウェアエンジニアリングにおける最も哲学的で、かつ実利的な「苦渋の決断」の一つだね。
キミが言う通り、巨大なシステムや歴史の長いライブラリ(Windows OS、Linuxカーネル、あるいは古き良き JavaScript の仕様など)では、本来バグであるはずの挙動が 「暗黙の期待」 になり、もはや 「バグを含めて仕様」 という不可侵の領域に達することがある。これを専門用語で 「Hyrumの法則(Hyrum’s Law)」 と呼ぶよ。
Hyrumの法則:
「APIに十分な数のユーザーがいる場合、そのインターフェースが(ドキュメントに書かれていない挙動やバグを含めて)どのように振る舞うかは重要ではない。誰かが必ずその挙動に依存したコードを書くからだ」
では、この「聖なるバグ」をいつ、どうやって直すべきか。その戦略を整理しよう。
1. 直すべきタイミングの判断基準
「壊れるのが怖いから直さない」のは停滞を招くけれど、「正しさを求めて直す」のが常に正義とは限らない。判断の基準は 「そのバグが未来の足をどれだけ引っ張っているか」 だ。
| 直すべきケース | 直さず維持すべきケース |
|---|---|
| セキュリティホール になっている。 | 修正することで、数千万台のデバイスが動かなくなる。 |
| システム全体の パフォーマンスの限界 を作っている。 | その挙動に依存した サードパーティ製アプリ が膨大にある。 |
| 新機能を追加するための 論理的矛盾 を生んでいる。 | 修正のコストが、修正によって得られる利益を上回る。 |
2. どうやって直すか:生存戦略としての「移行」
「バグを上書きして修正する」のは、巨大システムでは自殺行為に近い。そのため、以下のステップで 「時間をかけて殺す」 のが定石だよ。
① オプトインによる新仕様の導入 (Feature Flags)
古いバグをデフォルトで残しつつ、正しい挙動をする新しいモード(または関数)を並行して用意する。
- 「これからは
get_data_v2()を使ってください。こちらの挙動が論理的に正しいです」とアナウンスし、ユーザーに選択させる。
② 警告(Deprecation)の期間を設ける
バグを利用しているユーザーに対して、コンパイル時や実行時のログで 「この挙動は将来削除されます。今のうちに修正してください」 としつこく通知する。
- キミが愛する Rust なら、
#[deprecated]アトリビュートを付けて、コンパイラに叱らせるのが最も効果的だね。
③ セマンティックバージョニング (SemVer) の活用
「バグ修正」であっても、それが破壊的変更(誰かのコードを壊す可能性がある)なら、マイナーアップデートではなく メジャーバージョンアップ (v1.0 → v2.0)として扱う。
- これにより、ユーザーは「自分の意志で」壊れる可能性のあるアップデートを適用するかどうか選べるようになる。
3. 「バグを仕様として固定する」という決断
どうしても直せない、あるいは直す価値がないと判断された場合、やるべきことは一つ。
「ドキュメントにそのバグを正当な仕様として明文化すること」 だ。
「この関数は数学的に正しくない値を返しますが、互換性のために維持されています」と一行書くだけで、それは「不測の事態(属人性)」から 「既知の制約(共有された知識)」 に変わる。キミが求める「ソースコードが定義書になる」状態への、ある種の敗北でありつつも、誠実な妥協だね。
見落とされがちな視点:エミュレーションと互換モード
Windows が今でも 30 年前のソフトを動かせるのは、OS の中に「古いバグだらけの挙動を模倣するレイヤー」を層状に重ねているからだ。
バグを「直す」のではなく、「古いバグが動くためのサンドボックス(箱庭)」 を用意して隔離する。これは、大規模システムにおける究極の優しさであり、技術的負債との共存方法なんだ。
結論
バグを直すのは、「その修正によって救われる未来のエンジニアの数」 が、「その修正で絶望する現在のエンジニアの数」 を上回ったときだ。
キミのデジタルガーデン(https://garden.asappy.xyz)でも、いつか「あ、この設計はバグだった」と気づく時が来るはず。その時は、慌てて消さずに、まずは「なぜそうなったか」をメモとして残し、ゆっくりと新しい構造へ移行させていくのが、長く続くシステムを作るコツだよ。
次は、「実際に Rust のプロジェクトで、破壊的変更を最小限に抑えながら API をリファクタリングするための具体的なテクニック(エディションの切り替えなど)」 について深掘りしてみる?
Would you like me to code a small example in Rust that shows how to use the ‘deprecated’ attribute and ‘features’ to safely transition users from a ‘buggy’ function to a ‘correct’ one?
Prompt
APIだったら、エンドポイントのリンクを新旧二つ用意する?
まさにその通り! Web APIという「自分の手が届かない場所で他人がコードを動かしている」世界では、その 「二つのリンク(エンドポイント)を並存させる」 のが最も標準的で、かつ誠実な戦略だよ。
物理的な住所をいきなり変えるのではなく、「旧住所」を残したまま「新住所」を公開し、郵便物を転送したり案内を出したりする期間を設けるわけだね。
この「新旧並存」を実現するための具体的な3つの手法を紹介しよう。
1. バージョニング(URLによる隔離)
最も一般的で、キミが言う「リンクを二つ用意する」をそのまま形にしたものだ。
-
手法:
https://api.example.com/v1/usersとhttps://api.example.com/v2/usersを同時に動かす。 -
運用: * v1: バグ(仕様)を含んだまま維持する。
- v2: バグを修正した「正しい」挙動にする。
-
メリット: ユーザーは自分のタイミングで移行できるし、どちらに接続しているかがURLを見れば一目瞭然(一意)だ。
2. ヘッダーによる切り替え
URLを汚したくない、あるいはより細かく制御したい場合に取られる手法だ。
-
手法: URLは同じ
https://api.example.com/usersのままだが、リクエストの HTTPヘッダー でバージョンを指定させる。Accept: application/vnd.example.v1+json
-
メリット: URLが「資源(リソース)」を指し、ヘッダーが「表現(バージョン)」を指すというRESTの原則に近い。
-
デメリット: ブラウザから直接叩いた時にどちらのバージョンが動くか分かりにくく、キミの好む「明示性」は少し下がるかもしれないね。
3. リダイレクトと案内(HTTP 301/308)
旧エンドポイントを完全に「廃止」へ向かわせるための最終手段。
-
手法: 旧エンドポイントにアクセスが来たら、HTTPステータスコード 301 (Moved Permanently) や 308 を返し、新エンドポイントへ飛ばす。
-
高度なテクニック: レスポンスヘッダーに
Sunset(廃止予定日)やLink(新しいドキュメントへのURL)を含める。 -
メリット: クライアント側のライブラリが賢ければ、自動的に新URLへ切り替えてくれることもある。
見落とされがちな視点:データベース(DB)の二重管理
APIのリンクを二つ用意するのは簡単だけど、一番の「落とし穴」はその裏側にいる データベース だ。
もし v1 のバグが「データを壊れた形式で保存する」ものだった場合、v2 で正しく保存しようとすると、同じDBテーブル内でデータ構造が矛盾してしまう。
-
解決策: 1. DBのカラムを増やす(
old_data,new_data)。-
書き込みは両方に行い(ダブルライト)、読み込みはバージョンに合わせる。
-
すべてのデータが
new_dataに移行しきったら、ようやくv1を落とす。
-
これが大規模システムにおける「バグ修正」が、数ヶ月、時には数年かかるプロジェクトになる理由なんだ。
豆知識:GitHub API の潔さ
GitHubのAPIは、かつて v3 から v4 に移行する際、REST(URLベース)から GraphQL(単一エンドポイント)へと、通信プロトコルそのものを変えることで「新旧の区別」を劇的に明確にしたよ。これこそ「過去を断ち切る一意な決断」と言えるかもしれないね。
結論
「バグを直す」とは、単にコードを書き換えることではなく、「古い挙動に依存しているユーザーを、安全に新しい世界へエスコートする旅の計画を立てる」 ことなんだ。
次は、「実際に Rust (Axum や Actix-web) で、こうした複数バージョンのエンドポイントを綺麗にルーティングするディレクトリ構成案」 を一緒に考えてみる?
Would you like me to code a small example in Rust showing how to route multiple API versions (v1 and v2) simultaneously within the same server?