ライブラリ契約書
アプリケーションはその利用者によって評価される。 ライブラリはプログラマによって評価される。 しかも、それは作者が想像もしなかったことを利用者が求める瞬間に評価される。 良いライブラリとは、意図された使い方をしたときに便利なものではない。 それが、意図された使い方以外で使われても捨てられないものを言う。
以下のすべてのルールは、一つの非対称性から派生している。 ライブラリの内部では、あなたは何でも永遠に変更できる。 外部では、呼び出し元が観測できるものはすべて、誰かがそれに依存した最初の瞬間に凍結される。 そして呼び出し元が十分に多ければ、そのすべてに誰かが依存する。
何が、そしていつ凍結されるのか
ハイラムの法則は、単なる好奇心ではなく、動作の条件である。 十分な数の利用者がいる場合、観測可能な振る舞いはすべてが重荷となる。 例外の文言、列挙順序、ログ出力、タイミング、 さらに未定義として文書化された動作でさえも。 「それは契約に含まれていなかった」と言っても、それに対して出荷した呼び出し元を壊すことはできない。
したがって、凍結の境界は public キーワードではなく、観測可能性に位置する。
そして、三つの帰結が生じる。
- 表面は小さく始まる。 一度公開されたシンボルは、拡張はできても静かに撤回できない約束である。 「とりあえずpublic」は利益のない借金であり、要求されて初めて公開する。
- フォーマットはコードより長生きする。 ファイル形式、ネットワークプロトコル、 永続化スキーマは、ライブラリのあらゆる書き換えを生き延びる。 内部構造体をそのままシリアライズすることは、凍結された層を編集可能な層に溶接することである。
- アプリ内モジュールは、パッケージング時ではなく、2つ目の利用者が現れた時点でこの線を越える。 2つの呼び出し元が存在した瞬間、そのAPIは公開パッケージと同じ凍結の経済性を持つ。 爆発範囲が小さいだけである。
セマンティックバージョニングは、同じ線を公に綴ったものである。 そして、それなしでは機能しないことを宣言することから始まる。 「セマンティックバージョニングを使用するソフトウェアは、パブリックAPIを宣言しなければならない」(SemVer 2.0.0、第1条)。 一度宣言されれば、バージョンの引き上げは感じられるものではなく、決定されるものとなる。 そのAPIへの後方互換性のない変更はMAJORを上げなければならず(第8条)、 後方互換性のある新機能はMINORを上げなければならず(第7条)、 後方互換性のある修正はPATCHを上げなければならない(第6条)。 ハイラムの法則は、この宣言を困難にするもの、すなわち、 仕様はあなたが宣言したものを凍結し、 呼び出し元は彼らが観測したものを凍結する。
仕様には、まだ線を越えていないモジュールの名前もある。 「メジャーバージョンゼロ (0.y.z) は初期開発用である。何もかもがいつでも変更される可能性がある」(第4条)。 これは、利用者が1つしかいないモジュールに対する正直なラベルであり、 2つ目の利用者が現れた日が、パッケージ化された日ではなく、1.0.0を提供すべき日である。
制御を奪わない
メカニズムとポリシーを分離せよ。 HTTPリクエストの実行はメカニズムである。 それを指数バックオフで3回リトライするのはポリシーであり、 ポリシーをコアに組み込むと、ライブラリはそれを共有する呼び出し元に限定される。 ログ、スレッド、リトライ、タイムアウト、 キャッシュ、シリアライゼーションの便利なデフォルトを提供し、 そのすべてを置き換え可能にせよ。
アプリケーションの役割は唯一無二であり、それはあなたのものではない。 プロセスの終了、stdoutへの書き込み、SIGINTの捕捉、 トップレベルの例外ハンドラのインストール、スレッドプールのサイズ変更。 これらはプロセス内のただ一つのコンポーネントだけが行うべきであり、 それを行うライブラリは、その権限を持つコンポーネントと戦うことになる。 プロセスの入力の読み取りも同じ役割に属する。 環境変数、 設定ファイル、コマンドライン引数はアプリケーションルートで読み取られ、 ルートは各値をパラメータまたはオプションオブジェクトとしてライブラリに渡す。 呼び出し元は値の出所を選択でき、 テストではマシンをセットアップする代わりに値を渡す。
マジック(自動検出、自動変換、自動再接続、 自動キャッシュ)は、呼び出し元がなぜそれが行われたのかを再構築できる場合にのみ許容される。 デバッグできない便利さは、作業の節約ではなく、制御の奪取である。
隠れたコストを追加しない
物事がいつ起こるかは、呼び出し元の決定事項である。 スレッドを起動するインポート、コネクションを開くコンストラクタ、 環境変数を読み込む静的初期化子。 これらはそれぞれ、呼び出し元が選択したことのない、そして元に戻せない瞬間に実際の作業を移動させる。
依存関係はあなたのAPIの一部である。 そのランタイム、更新頻度、ABI安定性、ライセンス、 自身の初期化副作用はすべて、すべての呼び出し元に転送される。 あなたは彼らの代わりに選択しているのである。
パフォーマンス特性は契約である。 O(1)のルックアップをO(n)にすること、あるいはアロケーションとコピーを倍増させることは、 互換性のあるシグネチャをまとった破壊的変更であり、 呼び出し元はそれを本番環境で発見する。
入力が出力よりも速く到着する場所(ストリーム、イベント、ログ、 キュー)では、無制限の内部バッファは遅延したメモリリークである。 背圧は設計の一部であり、運用上の詳細ではない。
抽象化自体の基準はゼロコストである。 ストロウストラップの両方の条項において。 呼び出し元が使用しないものに対しては、支払いが生じてはならない。 便利な階層がプリミティブな階層に、そのアロケーション、 ディスパッチ、セットアップの負担をかけてはならない。 そして、呼び出し元が使用するものについては、手書きで合理的にそれ以上に優れたものを書くことができてはならない。 手書きのコードに負ける抽象化は回避され、 回避されたライブラリはバージョン番号をまとったまま捨てられる。
隠れた状態を保持しない
シングルトン、静的キャッシュ、グローバルイベントループ、 プロセス全体の設定。 これらは、あなたのライブラリだけがプロセス内にある間は機能するが、 別のライブラリ、プラグインホスト、 または並列テストスイートが共有すると衝突する。 状態は、呼び出し元が作成し、所有し、 二重に作成できるインスタンス内に存在する。
障害を隠さない
APIの形状は、何が起こり得るか、そして誰が責任を負うかを宣言する。 エラー、キャンセル、タイムアウト、部分的な成功、 不在。これらは型でモデル化され、 「これは通常は起こらない」という例外にルーティングされてはならない。 同じ形状は、コメントでは強制できない所有権の答えを伝える。 誰がストリームを閉じるのか、バッファを再利用してよいのか、 コールバックがどのスレッドに到着するのか。
原因を保持せよ。
障害を false や null に潰すことは、まさに上の層が判断するために必要だった情報を剥ぎ取る。
自分でログを取って処理を続けることは、同じ情報を失い、呼び出し元のログポリシーを奪う。
品質は異常系で差がつく。 正常系ではライブラリは互換性がある。タイムアウト、再接続、 ストリーム途中の切断、部分読み込み、 破損データ、リトライが差異の存在する場所であり、 したがって、それらの合成を最初に設計せよ。
キャンセルは後付けが困難である。キャンセルトークンは、それが触れるすべてのシグネチャを通過する。 I/Oおよび長時間実行操作は、最初のバージョンからそれを受け入れる。
逃げ道を塞がない
二つの階層を構築せよ。
一般的なケースを一行で処理する高レベルAPIを、公開プリミティブの上に置く。
ReadAsync() の上に ReadAsync(Memory<byte>) を。
これにより、トップ階層の範囲外の要件を持つ呼び出し元は、離脱する代わりに下りることができる。
高階層しか持たないライブラリは、予期していなかった最初の要件で捨てられる。
観測可能性は注入されるものであり、所有されるものではない。 ロガー、メトリクス、 トレースは呼び出し元のスタックにプラグインされる。 あなたのライブラリだけが読める診断フォーマットは、人質に取られたテレメトリである。
一つの税、二つの通貨
このスキルとクリーンアーキテクチャは、同じ拒否を二つの通貨で表現したものである。 ゼロコスト抽象化はランタイムパフォーマンスへの課税を拒否する。 依存関係ルールは変更容易性への課税を拒否する。 どちらにおいても、抽象化は依存側が何を諦めるかによって判断される。
| ここでの責務 | アーキテクチャ上の名称 |
|---|---|
| メカニズムとポリシーの分離 | ユースケースをフレームワークとドライバから分離する |
| デフォルトの置換可能性 | 依存性逆転 — 安定した側が実装を選ぶ |
| 便利な階層の下のプリミティブ | 外部の詳細を交換可能にする |
| グローバルな状態の排除 | 依存関係を明示的にし、境界を引けるようにする |
| 依存関係はAPIの一部 | 依存関係ルール |
呼び出し元の健全な形状は、あなたのライブラリを最下部に置く。 それは、彼らのポリシーとドメインの下にある、交換可能な詳細である。 逆転した形状 — アプリケーションがフレームワークの中に住み、 そのライフサイクル、その型、そのDI、そのエラーモデル、 その非同期モデルを引きずる — は、ライブラリがアプリケーションとして振る舞い、 アーキテクチャ規模で書かれたものである。
一般的な形式:優れたアーキテクチャは、依存関係の数を最小化するのではなく、 各依存関係が奪う自由度を最小化する。
命題
ライブラリにおける抽象化は、呼び出し元の選択肢を広げるために存在する。 それがパフォーマンス、制御、または情報を代償に取る瞬間、 それは抽象化をやめて決定を始めたのである。
適用すべきでない場合
利用者が1人、リポジトリが1つ、 チームが1つのモジュールは、まだ凍結の線を越えていない。 それはSemVerの用語で0.y.zの状態であり、 数字を持っているかどうかに関わらない。 そのようなモジュールのためにすべての置換可能ポリシーの継ぎ目を構築することは、 メンテナンス費用がかかる憶測である。 最初の日からコストのかからない規律(グローバル状態なし、 原因を保持する障害、 呼び出し元が要求する前の作業なし)を守り、 2つ目の利用者が現れたときに継ぎ目を追加せよ。 その瞬間、それらは憶測ではなくなるのである。
出典
- Semantic Versioning 2.0.0, semver.org: 第1条(パブリックAPIは宣言されなければならない)、 第6~8条(PATCH、MINOR、MAJORの引き上げ)、第4条(0.y.z)、 および非推奨に関するFAQエントリ(それを文書化し、非推奨を配置したマイナーリリースを発行する)。 文言はページに対して確認済み。
- ハイラムの法則は標準ではなく観察であり、一つの引用として挙げられている。