Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9541

DIコンテナのつらみを補うミドルウェア "Deptorygen"

$
0
0

C#向けのAnalyzer+CodeFixライブラリを作ってみました。

https://github.com/NumAniCloud/Deptorygen

リポジトリ内にあるマニュアルを基にした紹介をしたいと思います。

対象読者

DIコンテナを使用してある程度大きなソフトウェアを開発したことのある方に向けています。

DeptorygenはC#用のアナライザーなので、C#を使用している方に向けています。

既存のDIコンテナとしてGenericHostを例に挙げているので、GenericHostを知っていると理解がスムーズかもしれません。

Deptorygenとは

Deptorygen(でぷとりじぇん)はC#向けのAnalyzer+CodeFixライブラリです。現在、Nuget経由で入手することができます。

Deptorygenの主な役割は、従来のDIコンテナの弱点を補うことです。従来のDIコンテナとはGenericHostのDIコンテナ機能のようなものを指しています。こういったミドルウェアは、インスタンスの依存関係を解決するために動的な処理を用います。それは時に動的コード生成だったり、リフレクションだったりします。

GenericHostにおける注入の設定の書き方
varservices=newServiceCollection();services.AddSingleton<IService,ServiceGold>();services.AddTransient<Client>();

一方で、Deptorygenはいわば静的なDIコンテナであり、コンストラクタインジェクションは本当にコンストラクタにインスタンスを注入するようなnew式としてコード生成されます。

静的コード生成で作られるのはファクトリーパターン的な振る舞いをするクラスで、そのクラスの機能はDeptorygenが「ファクトリー定義」と読んでいるような形式のインターフェースとして書かれます。

Deptorygenにおける注入の設定の書き方
[Factory]interfaceIFactory{[Resolution(typeof(ServiceGold))]IServiceResolveService();ClientResolveClient();}

依存関係を静的に解決するDeptorygenは万能ではなく、コンパイル時に型の判明しているものにしか使えません。かといって、依存関係を動的に解決する方法は時に強力すぎで、それを制御するコストが高くつくこともあります。Deptorygenと動的なDIコンテナを組み合わせて使うことがベストな使い方であると考えています。

静的コード生成の利点

不透明さの解消

動的に依存関係を判断してインスタンスを生成する場合、その手順は実行時に決まります。こうなると、プログラマーはインスタンスが実際にどのような手順で生成されているのかを知ることができません。

// この2つのクラス Service, Client の生成をDIで行いたいclassService{}classClient{publicClient(Serviceservice){}}classProgram{publicstaticvoidMain(){// 生成したい型の情報を登録するvarservices=newServiceCollection();services.AddSingleton<Service>();services.AddTransient<Client>();varprovider=services.BuildServiceProvider();// 利用側varclient=provider.GetService<Client>();// →それで、どんな手順でインスタンスを生成しているのだろう?// 動的に依存関係が解決されるので分からない}}

Deptorygenでは、インスタンスを生成するコードが静的にコード生成されるため、そのコードを見ればどのような手順で生成されているのかを理解することができます。

ファクトリーを生成してみよう

以下はユーザーの書くコードです。

usingSystem;usingDeptorygen.Annotations;usingUseDeptorygen.Infra;namespaceUseDeptorygen.Samples.BasicDependency{// newしたいクラスたちclassService{publicvoidShow(){Console.WriteLine("This is Service!");}}classClient{privatereadonlyService_service;publicClient(Serviceservice){_service=service;}publicvoidExecute(){Console.WriteLine("# Client");_service.Show();}}// インターフェースに Factory 属性をつけたものが「ファクトリー定義」[Factory]interfaceIFactory{ServiceResolveService();ClientResolveClient();}classProgram{publicstaticvoidMain(){varfactory=newFactory();factory.ResolveClient().Execute();}}}

IFactoryの部分にVisual Studioからクイックアクションが提供され、Deptorygenによるコード生成コマンドを実行することができます。

以下は生成されるコードです。

// <autogenerated />#nullableenableusingSystem;usingSystem.Collections.Generic;namespaceUseDeptorygen.Samples.BasicDependency{internalpartialclassFactory:IFactory,IDisposable{privateService?_ResolveServiceCache;privateClient?_ResolveClientCache;publicFactory(){}publicServiceResolveService(){return_ResolveServiceCache??=newService();}publicClientResolveClient(){return_ResolveClientCache??=newClient(ResolveService());}publicvoidDispose(){}}}

生成されたFactoryクラスではいくつかの機能をサポートしています。

  • ServiceクラスをnewするResolveServiceメソッド
  • ClientクラスをnewするResolveClientメソッド
  • newしたService,Clientインスタンスはキャッシュする
  • キャッシュしたインスタンスがIDisposableなら、それをまとめてDisposeできる機能
  • ファクトリー定義として使用したインターフェースを実装している
  • 必要な名前空間があればusingする

ガイド:基本的な使い方
↑話題に関連するマニュアルのページへのリンクを張っておきますので参考にしてください。

無効な設定にコンパイルエラーを出す

動的に依存関係を判断してインスタンスを生成する場合、依存関係を解決する手段が実行時に決まるということなので、実際には依存関係を解決できないような設定でDIコンテナが使用されている場合にコンパイルエラーを出すことができません。

ここからはMainメソッドを省略した簡易的なコードで紹介します。トップレベルに処理が書かれていたら、それはMainメソッドの内部です。

classService{}varservices=newServiceCollection();// DIコンテナに Service を登録するのを忘れちゃった// services.AddSingleton<Service>();varprovider=services.BuildServiceProvider();// 利用側// ここが実行時エラーになる(コンパイルエラーにならない)varservice=provider.GetService<Service>();

Deptorygenを用いてファクトリークラスを生成すると、依存関係の解決が不可能であった型に対しては無効なコードが生成され、コンパイルエラーとなります。

classService{}[Factory]interfaceIFactory{// Service 型に対するメソッドを書き忘れちゃった}// こんな感じのファクトリーが生成される(機能がからっぽ)publicclassFactory:IFactory,IDisposable{publicvoidDispose(){}}// 利用側varfactory=newFactory();// ResolveServiceなるメソッドは存在しないのでコンパイルエラーが出るvarservice=factory.ResolveService();

ただし、依存先のインスタンスを生成することができないことにより依存関係の解決が不可能であった場合はファクトリークラス自体は有効なコードが生成されます。

その代わり、足りない依存先がコンストラクタの引数でもって利用者に対して要求されます。この引数にインスタンスを渡したくない場合はファクトリーも生成できないことになるため、プログラマーはファクトリークラスに対して十分に型の情報を伝える必要があることに気づくことができます。

classService{}classClient{publicClient(Serviceservice){}}[Factory]interfaceIFactory{// Service 型に対するメソッドを書き忘れちゃった//  Service ResolveService();ClientResolveClientAsTransient();}// 生成されるコードinternalclassFactory:IFactory,IDisposable{privatereadonlyService_service;// 足りない依存先はコンストラクタで外部に要求するpublicFactory(Serviceservice){_service=service;}publicClientResolveClientAsTransient(){// Service に対するメソッドが無いので、仕方なくフィールドに持ってるインスタンスを使う// フィールドの中身は、コンストラクタを通じて渡される前提returnnewClient(_service);}publicvoidDispose(){}}

ガイド:コンストラクタで意外な引数を要求されたら

追加の引数を与える

動的に依存関係を判断してインスタンスを生成するDIコンテナでは、実際にインスタンスを生成するタイミングで初めて得られるような情報を追加で引数に渡して、適切に設定されたインスタンスを生成できるものもあります。しかし、こうして与える追加の引数についてコンパイル時に型チェックをしてもらうことは困難です。

// これは今からnewしたいクラスclassService{publicService(stringmessage){}}varservices=newServiceCollection();services.AddSingleton<Service>();// ここでは引数に関する情報を伝えないvarprovider=services.BuildServiceProvider();// 生成時に引数を渡せる。ただしprovider.GetService<Service>() という書き方はできない// 引数が (ServiceProvider, params object[]) なのでIntellisenseも効かないvarinstance=ActivatorUtilities.CreateInstance<Service>(provider,"SomethingMessage");// 型チェックがないので、stringを渡すべき場所に何でも渡せてしまう// これは実行時エラーになるvarinvalid=ActivatorUtilities.CreateInstance<Service>(provider,DateTime.Now);

DeptorygenでもそうしたDIコンテナと同様に、インスタンスを生成するときに追加の引数を渡すことができます。ただし、依存関係を解決するコードは静的に生成されているため、追加で渡す引数も必ず型チェックの対象となります。

// これは今からnewしたいクラスclassService{publicService(stringmessage){}}[Factory]interfaceIFactory{// ファクトリー定義の時点で引数の情報を伝えておくServiceResolveService(stringmessage);}// 生成されるクラスは以下のような感じinternalpartialclassFactory:IFactory,IDisposable{privateService?_ResolveServiceCache;publicFactory(){}publicServiceResolveService(Stringmessage){return_ResolveServiceCache??=newService(message);}publicvoidDispose(){}}varfactory=newFactory();// 生成時に引数を渡せる。静的コード生成なのでIntellisenseも効くvarinstance=factory.ResolveService("SomethingMessage");// 型チェックが効くので、これはコンパイルエラーになるvarinvalid=factory.ResolveService(DateTime.Now);

サンプル:解決メソッドに直接オブジェクトを渡す

インスタンスの自由な寿命管理

DIコンテナにおいてインスタンスの寿命を直感的に管理するのは難しい課題です。筆者の利用したものの多くは、インスタンスの寿命はSingleton, Scope, Transientといった3つ程度の区分に分かれ、あとはDIコンテナ独自のクラス構造を駆使してスコープや寿命を管理します。

例えばGenericHostのDIコンテナであれば、ServiceProviderのインスタンス1つが1つのスコープに対応しています。

Deptorygenでは、インスタンスの寿命はそのインスタンスをキャッシュしているファクトリーが基準となります。ファクトリークラスはstaticなものではないし、DIコンテナとしての特別な機能が備わっているクラスでもないので、依存関係を注入する対象のクラスたちと同様に取りまわすことができます。もちろん、ファクトリーがファクトリーを生成することも可能です。

Deptorygenでのインスタンスの寿命は2種類です。Cached……つまりファクトリーそのものと同じ寿命か、Transient……生成するたびに違うインスタンスか、です。

ファクトリー自体をシングルトンにするのも自由です。その場合、寿命がCachedであるインスタンスもシングルトンな寿命を持つことになります。ファクトリーのコンストラクタがprivateになることをファクトリー定義で指示することが現状ではできないので、シングルトンにするには別のクラスに包含させる、あるいはファクトリー自体をDIコンテナに生成させるなどの工夫は要りそうです。

他にも、ファクトリークラスを生成する種となる複数のインターフェース定義のあいだに継承や包含の関係を持たせれば、複数のファクトリー間でキャッシュを共有したり、特定のインスタンスを生成する権利を持つクラスを限定するなどの使い方ができたりなど、DIコンテナを使わない場合と同じくらいに寿命とスコープを柔軟に管理することができます。

ガイド:ファクトリーを別のアセンブリに提供する

サンプル:依存関係の解決に別のファクトリーも利用する(キャプチャ)

動的な依存解決との組み合わせ

プラグインで拡張のできるアプリなどを開発していると、外部からどのようなクラスが供給されるのか不明な場合があります。

特に、2つのプラグイン間で依存関係が存在する場合は困難な問題になります。どのようなクラスが供給されるのかだけでなく、どのようなクラスが要求されるのかすら不明なため、静的に依存関係を解決できる可能性は絶望的です。

こうなった場合、動的な依存解決の出番です。

Deptorygenは現在GenericHostのDIコンテナと連携する機能があり、Deptorygenの生成したファクトリークラスをGenericHostが依存解決する際に利用するよう登録できます。

以下はユーザーの書くコードです。

// newしたいクラス Service, Service2, ClientclassService{}classService2{}classClient{publicClient(Serviceservice,Service2service2){}publicvoidWork(){/* service, service2 を使って何かする */}}// ConfigureGenericHost 属性をつけると、GenericHostで使えるようになる[Factory][ConfigureGenericHost]interfaceIFactory{ServiceResolveService();Service2ResolveService2();ClientResolveClient();}classGenericHostSample{publicvoidRun(){varservices=newServiceCollection();// GenericHost の ServiceCollection インスタンスに、ファクトリーのインスタンスを登録するservices.UseDeptorygenFactory(newFactory());varserviceProvider=services.BuildServiceProvider();// Factory クラスで解決できる依存関係が、ServiceProvider からも解決できるようになるserviceProvider.GetService<Client>().Work();}}

以下のようなコードが生成されます。

// <autogenerated />#nullableenableusingSystem;usingSystem.Collections.Generic;usingDeptorygen.GenericHost;usingMicrosoft.Extensions.DependencyInjection;namespaceUseDeptorygen.Samples.GenericHost{internalpartialclassFactory:IFactory,IDisposable,IDeptorygenFactory{privateService?_ResolveServiceCache;privateService2?_ResolveService2Cache;privateClient?_ResolveClientCache;publicFactory(){}publicServiceResolveService(){return_ResolveServiceCache??=newService();}publicService2ResolveService2(){return_ResolveService2Cache??=newService2();}publicClientResolveClient(){return_ResolveClientCache??=newClient(ResolveService(),ResolveService2());}// GenericHostと連携するためのメソッドpublicvoidConfigureServices(IServiceCollectionservices){// キャッシュはファクトリー側が管理するので、すべてTransientservices.AddTransient<IFactory>(provider=>this);services.AddTransient<Service>(provider=>ResolveService());services.AddTransient<Service2>(provider=>ResolveService2());services.AddTransient<Client>(provider=>ResolveClient());}publicvoidDispose(){}}}

この Factoryクラスは IDeptorygenFactoryを実装しています。クラスがIDeptorygenFactoryを実装していると、UseDeptorygenFactory拡張メソッドに渡すことができます。

ConfigureServicesメソッドがIDeptorygenFactoryの実装に必要なAPIです。

サンプル:GenericHostと連携する

まとめ

Deptorygenのコンセプトは、「静的に解決できる部分だけでも静的に解決する」です。依存関係を静的に解決することで生まれるいかなる潜在能力にぼくが期待しているかは、この記事には書ききれません。もういくつか紹介の記事を書くかもしれませんが、おそらくリポジトリに用意したマニュアルと同程度の紹介になると思います。

興味のある方はDeptorygenを使ってみてください。Twitterなどで感想・要望をもらえると嬉しいです。GitHubのissueを通じて要望を受け付ける予定はありませんが、issueを立ててもらってもそれほど困らないのでどうぞ。

Deptorygenにはまだ細かい問題が残っており、使いづらいこともあるかもしれません。反響があればディスカッションの類はSlackを立てて、そこでしたいかなと思っています。

それと……依存関係を静的に解決するというアイデアは別の言語にも適用できるだろうし、Deptorygenとは違った実装をC#に与えることもできると考えています。同じアイデアのミドルウェアがあれば教えてください。そして、皆さんもこのようなミドルウェアを作ってみると面白い挑戦になるかもしれません。


Viewing all articles
Browse latest Browse all 9541

Trending Articles