依存性の注入と C# 11
C# 11 で static abstract
なメンバーをインターフェイスに追加できるようになるのはすごく良い。ずっと欲しかったヤツ。
たとえば MyContainer
というクラスがあったとして、登録される側のクラスに static void Register()
を強制する手段が今まではなかった。(Obsolete
で無理矢理下線引いたりしてた)
--
いまいち恩恵を感じないまま色々書いてみたけど、もしかしたらアトリビュート付けるだけ系の DI がダメなのかもしれない。Autofac は何もつけずに動みたいだけど、見てて抵抗がない。グローバル変数って感じ。※ この記事内で扱っている「依存性の注入」は、Unity 向け DI コンテナーにしか存在しない
[Inject]
の利用を前提にしています。[Inject]
じゃなくて [global]
だったらすんなり DI 受け入れられるのかも。
依存性注入
Dependency Injection の良くある感じとしては、
// DIコンテナ
Register<T, T>();
Register<T, T>(Nanchara).AsNice();
Register()
.AsSingleton<T>()
.LazyInitialize();
...
// 注入
[Inject] Something m_something;
static abstract
+ ModuleInitializer
これが、C# 11 の static abstract
(と C# 9 の ModuleInitializer
の組み合わせ)で、
// 注入 <Something>() 消したい
Something m_something = DI.Inject<Something>();
// 自作コンテナ 名前が DI.Inject なだけでロケーターとかプロバイダー、ファクトリー系か??
static class DI {
static T Inject<T>() where T : IInjectable, new()
{
if (isDebug)
...
else
...
}
static void Register<T>() where T : IInjectable, new() {}
}
// 登録
internal class Something : IInjectable
{
[ModuleInitializer] // これもインターフェイスで保証したいね
internal static void Register() => DI.Register<Something>();
}
// インターフェイス
internal interface IInjectable {
abstract internal static void Register(); // C# 11
}
// 👇 は出来ない。残念!
internal class IInjectable<T> where T : class
{
[ModuleInitializer] //error
internal static void Register() => DI.Register<T>();
}
Register
羅列は各クラスに分散。静的抽象インターフェイスのおかげで実装漏れもない。Inject
処理は引き続き集中管理、こまかい挙動は自分で自由に作れるようになる。
いわゆるエントリーポイントも不要になり、Unity 的はどのシーンから入るか問題がなくなる。助かる。
※ ModuleInitializer
に実行順を制御する方法がないのがちょっと不安。Unity なら RuntimeInitializeOnLoadMethod
のが実行順の制御ができるから便利か。
-
AfterSceneLoad
: After Scene is loaded. -
BeforeSceneLoad
: Before Scene is loaded. -
AfterAssembliesLoaded
: Callback when all assemblies are loaded and preloaded assets are initialized. -
BeforeSplashScreen
: Immediately before the splash screen is shown. -
SubsystemRegistration
: Callback used for registration of subsystems - https://docs.unity3d.com/ja/2021.3/ScriptReference/RuntimeInitializeLoadType.html
テスト時
分散したのは Register 処理だけ。引き続き Inject
で注入を集中管理出来てるから、全部テスト用のものに差し替えればおっけ。
メソッドへの注入
DIコンテナならできるメソッドの引数に注入するってのはこのやり方だと出来ない。使いどころも良くわからないけど。DIコンテナへの依存を代償にフィールドを少なくできるだけ??
おわりに
DIって依存関係を一か所に羅列しなきゃいけないことが強制される状態になって、結果的に見通しが良くなるのが利点なんかね。
[Inject]
と書くだけじゃダメだけど、その時点でエラーは出ない。エントリー内で忘れずに Register しなきゃいけない。Register 不要になった場合はエントリー内で該当部分を消す。でも [Inject]
付きのフィールドは残る。うーーん。散らかり方が変わっただけで片付いていない感じ。
依存してる部分が減る、注入されてもされなくても見かけ上エラーは出ないしコンパイルは通る、けど実際は注入されないとまともに動かないんだから、ロケーターなのかプロバイダーなのかに依存しても良くないか? 引数ありで取得するオブジェクトの調整ができます、さえなければイイじゃんね。
デカくなった DI クラスを後々小分けにしなきゃいけない時にちょっと手間かかるかもだけど、C# みたいな高級言語はリファクタリング楽だし問題なさそうに思える。
注入部分が数百数千あったとしたら DI の入口の Inject は維持して、そこから種類別の InjectResolver に振り分ける形で小分けにするで良さそうな気はする。場当たり的で適切ではない、とか言われるとつらいが。
--
アトリビュート付けるだけってのが怖いし、実行までエラー出ないし(?)処理の流れを辿りづらい。やってることに対してセットアップのおまじない冗長すぎ。
DI分からん ☕
以上です。お疲れ様でした。