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/usershttps://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 )。

    1. 書き込みは両方に行い(ダブルライト)、読み込みはバージョンに合わせる。

    2. すべてのデータが 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?