Guide / Reinhardt Reinhardt 0.4.0-alpha.13; moonc 0.10.11+6ff76a5f9
好きな言語で依存性注入システムを作るには?
この記事の内容
好きな言語で依存性注入システムを作る仕組みを調べます。 この動画ではMoonBitを実装例に使い、Reinhardtの依存関係の解決とscopeの規則を再現します。小さく動く核を作り、好きな言語へ移す際に保つデータ構造と挙動の検査を説明します。
RustとMoonBitの実験をダウンロード。展開してframework-internals/README.mdに従ってください。Rust 1.96.0、Moon CLI 0.1.20260827、moonc 0.10.11+6ff76a5f9、Node 22.22.1、Python 3.12、ネイティブのCツールチェーンで検証しています。初回ビルドでは依存を取得します。SQLiteの実験はそれぞれ別のメモリ内DBを使い、サーバーは不要です。
python3 verify.pyGive each dependency the right lifetime
2つのリクエストで同じ設定を使いつつ、それぞれ固有のリクエスト状態が必要だとします。 Reinhardtはシングルトンを共有してリクエストの値を分け、MoonBitでもその同一性を再現します。 実際の非同期の登録表を読み、値を作るか再利用するかを決める規則を作ります。
依存性注入は、サービスが必要とする値を外から渡し、その構築方針を見えるようにする仕組みです。 参照するのはReinhardt 0.4.0-alpha.13で、再現するグラフは意図的に小さくしています。
Separate a recipe from the object it creates
プロバイダーは、1つの依存を作る方法を知っている関数です。 Reinhardtの登録表は、要求するRustの型をキーにして、プロバイダーとスコープの方針を保存します。 Settingsのプロバイダーは共有設定を返し、ServiceのプロバイダーはSettingsとRequestStateを解決します。
解決とは、構築方法や再利用できる値を探し、要求された依存を呼び出し元へ渡すことです。 サンプルでは実際の非同期プロバイダーを登録し、実際のInjectionContextの解決処理を呼びます。 登録表が定めるのは構築の方針であり、テスト用の値に名前を付けても本物のDB接続になるわけではありません。
グラフが固定なら、明示的なコンストラクター引数でも依存を表せます。今回は、どの解決にも同じ再利用規則を適用する場所としてコンテナを使います。
実行可能なソースからの抜粋です。プログラム全体はダウンロードに含めています。
registry.register_async(DependencyScope::Singleton, |_| async {
Ok(Settings("demo"))
});
registry.register_async(DependencyScope::Request, |_| async { Ok(RequestState) });
registry.register_async(DependencyScope::Request, |ctx| async move {
Ok(Service {
settings: ctx.resolve().await?,
request: ctx.resolve().await?,
})
});Choose where successful values are cached
固定した登録表には、Singleton、Request、Transientという3つの再利用方針があります。 同じSingletonScopeを使う2つのコンテキストは、同じSettingsを再利用できます。 Requestの値は各リクエストのキャッシュに入り、同じリクエスト内で繰り返し解決すると同じオブジェクトを使います。
2つ目のリクエストは別のキャッシュを持ち、別のRequestStateを受け取ります。 Transientでは再利用せず、解決するたびに新しい値を作ります。 これらの規則は値を誰が共有するかを定めます。リソースの終了や呼び出し元が保持する参照は、別の問題です。
Compare identities, not just equal contents
比較を実行し、両方の言語で観測したスコープの動作を見ます。 1つのリクエスト内でServiceを2回解決すると、同じオブジェクトが返ります。 2つのリクエスト間ではSettingsを共有し、RequestStateは別になります。
Transientを2回解決した場合も、別々のオブジェクトになります。 RustではArcのポインタ同一性を比較し、MoonBitでは生成時のIDと変更の共有を確認します。 同じラベルだけでは再利用の証明にならないので、MoonBitで返された値の変更が、キャッシュから得た別の参照にも見えることを確かめます。
構築回数も数え、未登録のプロバイダー、循環、不適切な寿命の依存も検証します。 キャッシュにあれば構築関数を再び呼ばない必要があります。Transientの別々のIDで構築を確かめ、変更の共有で、Requestの再解決が構築済みの値を返すことを確かめます。
Rust / Reinhardt
same_request_service=true cross_request_state=false
cross_request_singleton=true transient_reused=false
missing=rejected cycle=rejected captive_request=rejected
PASS reinhardt-di=0.4.0-alpha.13; async factories and real scopesMoonBit
same_request_service=true cross_request_state=false
cross_request_singleton=true transient_reused=false
missing=rejected cycle=rejected captive_request=rejected
PASS MoonBit async factories; shared singleton and separate request cachesRebuild the async resolver in MoonBit
MoonBitの登録表では、RustのTypeIdやAnyの代わりに、キーを限定したenumを使います。 各プロバイダーは、非同期の構築関数と3つのスコープ方針のいずれかを持ちます。 解決処理はスコープの規則とキャッシュを確認し、循環を検出してから構築関数の完了を待ち、成功した値を保存します。
MoonBitの非同期呼び出しはここでは通常の呼び出し構文を使い、固定したasyncライブラリを通して実際に処理を譲ります。 再帰呼び出しは適切なキャッシュを共有しつつ、それぞれの経路を受け取り、エラーで古い構築中の印を残しません。 これは動作するスコープ方針の再現で、完全な型付きコンテナではなく、1種類のインスタンス表現を使います。
実行可能なソースからの抜粋です。プログラム全体はダウンロードに含めています。
let child = { ..self, path: self.path + [key], }
let value = (provider.create)(child)
if cache is Some(cache) {
cache[key] = value
}
value
}Reject cycles and captive request values
構築が終わる前にAがBを、BがAを要求すると、成功したキャッシュではその循環を解決できません。 現在の経路から繰り返されたキーを検出し、無限に再帰せずエラーにします。 別の不適切なグラフとして、SingletonのプロバイダーがRequestの値を保持しようとする場合があります。
長く生きる値がリクエスト固有の状態を取り込むため、両方とも検証した依存関係を拒否します。 これらの失敗の後も、先ほどのServiceを解決すると、キャッシュした値を返します。 重複登録も設定エラーですが、形は異なります。Reinhardtはpanicし、MoonBitの登録表はエラーを送出します。
Keep the lifetime contract testable
付属のグラフから始め、スコープを明示的に選んだ依存を1つ加えてください。 実行前に、リクエスト内で共有するか、リクエストをまたぐか、再利用しないかを予想します。 観測結果が予想と違うときは、リンクしたReinhardtの解決処理をたどってください。
MoonBitの検証は順番に解決するもので、並行した構築、完全なキャンセル動作、リソース解放は確認していません。 キーを限定した構成なので、フレームワーク全体の型付き抽出や上書き機能も省いています。 Reinhardt Pressでは両方のソースとassertを公開し、小さな寿命の疑問を再現できる調査につなげます。
好きな言語へ仕組みを持ち帰る
好きな言語で、依存関係にはキー、構築にはfactory関数、選んだ寿命にはMapを使います。非同期呼び出しと共有参照を、その言語のランタイムに合わせます。
観測できる規則を保ちます。scope内で再利用し、リクエストの状態を分離し、依存の欠落や循環を拒否し、成功した結果をキャッシュします。
ソースの対応と検証の限界
PORTING.mdに、固定したフレームワークの関数と実行できるMoonBit実装の対応、および省略した機能を記載しています。ランタイムの表現は異なり、完全な移植ではなく範囲を限定した調査です。Node 22のSQLite APIではexperimentalの警告が出ます。