静的なDIコンテナ「Imfact」

6 min read読了の目安(約5500字

本記事では、ナムアニクラウドの開発したC#向けソースジェネレータである「Imfact」を紹介します。

ソースジェネレータ「Imfact」は、NuGetから入手可能です。ぜひ使ってみてください。

GitHubのリポジトリ上のマニュアルもご覧ください。マニュアルは随時追記予定です。

概要

GenericHostなどの既存のDIコンテナは、プログラム実行中にオブジェクトの依存関係を解決するため、使い方が間違っていても実行するまで判明しないなどの問題があります。

今回紹介するソースジェネレータ「Imfact」は、オブジェクトの依存関係をコンパイル時に解決します。これにより、使い方に間違いがあれば実行前に分かるなどのメリットがあります。

本記事にはImfactのコンセプトや内部実装についての話題も含まれますが、とにかく早いところImfactを試してみたい方は絵文字💉が付いている節だけを読んで、他の節は気になったときに読んでみてください。

ソースジェネレータImfactのはたらき💉

Imfactは、以下のMyFactoryクラスのようなクラス定義からファクトリーパターン的な実装を生成するソースジェネレータです。

[Factory]
partial class MyFactory
{
	// 生成したい型ごとにメソッドを宣言する(名前は自由)
	public partial Service ResolveService();
	public partial Client ResolveClient();
}

// Client, Serviceの構造は以下のような感じ
class Client
{
	public Client(Service service)
	{
	}
}

class Service
{
}

コード生成が済むと、例えばMyFactory.ResolveClientメソッドを呼び出すだけでClientクラスを生成でき、Clientクラスがどんなクラスに依存しているのかを気にする必要はありません。

MyFactoryのようなクラスを「ファクトリークラス」といいます。MyFactoryクラスも通常のクラスですので、MyFactoryクラスのインスタンスを取り回したり、必要な場所で生成したりする使い方もできます。使用感としては既存のDIコンテナよりも、どちらかと言えば FactoryMethod パターンのほうが近いです。

どんな手順で解決しているのか

上記のコードに対して、生成されたファクトリーの実装は以下のようにClientを生成しようとします。

ResolveClientメソッドの中身
Client? result;
result = new Client(this.ResolveService());
return result;

Clientクラスを生成するためにコンストラクタを呼び出していますが、コンストラクタを呼び出すためにはServiceクラスのインスタンスが必要ですので、それを生成するために自分自身の持つResolveServiceメソッドを呼び出しています。

ResolveServiceメソッドの生成される実装は以下のような感じです。

ResolveServiceメソッドの中身
Service? result;
result = new Service();
return result;

こちらもServiceクラスのコンストラクタを呼び出しています。このコンストラクタはClientとは違ってパラメータを持たないので、これ以上他のメソッドを呼ぶことは特になく、依存関係の解決が完了します。

そもそも、動的なDI・静的なDIとは?

C#にソースジェネレータのシステムが整備された昨今、DIコンテナ的な処理を静的に行おうというモチベーションが高まっています、たぶん。ぼくが注目している例では、StrongInjectというソースジェネレータが今後人気を高めていきそうに思います。

ここでいう「動的なDI」とは、オブジェクトの依存関係を解決する処理が実行時に走るようなDIの仕組みを指しています。「静的なDI」とは、同様の処理をプログラムの実行より前に行うようなDIの仕組みを指しています。

今までのところ、C#で使われているDIコンテナの多くは実行時に依存関係を解決していることでしょう。Zenject, Ninject, GenericHostなどがそれにあたります。こういったライブラリは強力ですが、以下のような問題があります。

  • 実行時にオブジェクトの依存関係を探索するため、実行時に負荷がかかる
  • 使い方を間違えていても、実行するまでエラーが知らされない
  • オブジェクトを生成するときにどんな処理が走っているのかが分かりづらい

もし、オブジェクトの依存関係を実行前に解析することができれば、上記のような問題が解決されるでしょう。 具体的には以下のようになります。

  • 実行より前にオブジェクトの依存関係を探索するため、実行時に負荷がかからない
  • 使い方を間違えると、コンパイル時にエラーが出る
  • オブジェクトを生成するときにどんな処理が走っているのかを知りたければ、生成されたコードを読めばいい

Imfactの主な機能たち💉

Imfactには直感的に使える便利な機能が備わっています。詳しい使用方法については、マニュアルをご覧ください。

使い方のミスに気づきやすい

最初の例をもう一度出してみます。ただし今度は、ResolveServiceメソッドを書くのを忘れたものとします。

[Factory]
partial class MyFactory
{
	public partial Client ResolveClient();
}

// Client, Serviceの構造は以下のような感じ
class Client
{
	public Client(Service service)
	{
	}
}

class Service
{
}

このとき、MyFactoryのコンストラクタをImfactが生成しますが、このコンストラクタではServiceクラスのインスタンスが要求されます。MyFactoryクラスを使う側がこの状態を望んでいなければ、引数を指定していないためコンパイルエラーが出るという仕組みになっています。

コンストラクタで意外な引数が要求されて困ったときは、ファクトリークラスのメソッド定義が欠けていないか確認してみましょう。Imfactがもっと情報が欲しいと言っているのかもしれませんから。

派生型の注入

ある型Aのオブジェクトを取得するメソッドが、実際には型Aの派生型を返すような設定ができます。DIコンテナではおなじみの機能です。

キャッシュ

生成したインスタンスをキャッシュして、ファクトリーのメソッドを何度読んでも同じインスタンスを返すようにできます。他のDIコンテナではSingletonと呼んでいるものと近い使い道で使えます。

継承と委譲

ファクトリークラスどうしの間に継承関係や包含関係を持たせることができます。各ファクトリーは、依存先のインスタンスを生成するために、基底クラスのメソッドや保持しているクラスのメソッドも候補に入れます。

この機能によって、ファクトリークラスを複数組み合わせて大きなファクトリークラスを構築することもできますので、インスタンスを生成する手順を柔軟に組み合わせることができます。

たいへん基本的な文法である「継承」と「包含」を、依存関係の解決手順を組み合わせる手段として利用できるという点で、Imfactは直感的な使用感が実現できていると思います。

フック

ファクトリークラスのメソッドが実行されるとき、その処理の前後にユーザー独自の処理を挟むことができます。特定のクラスが生成されるタイミングでログを出したい場合などに役立つことでしょう。

ちなみに、キャッシュ機能はフック機能を利用して実装されています。

実際に生成されるコードの全貌

生成されるコードの全貌が分かると安心すると思うので、ここに載せておきます。冒頭で出てきたMyFactoryの例では、以下のようなコードが生成されます。

partial class MyFactory
{
	private protected ResolverService __resolverService;

	public MyFactory()
	{
		__resolverService = new ResolverService();
		this.RegisterService(__resolverService);
	}

	internal void RegisterService(ResolverService service)
	{
		__resolverService = service;
	}

	public partial Service ResolveService()
	{
		using var scope = __resolverService.Enter();
		Service? result;

		result = new Service();

		return result;
	}

	public partial Client ResolveClient()
	{
		using var scope = __resolverService.Enter();
		Client? result;

		result = new Client(this.ResolveService());

		return result;
	}
}

動的なDIとの使い分け

静的なDIは万能ではなく、動的なDIでないと解決できない依存関係も一応あります。例えば、アプリケーションにプラグイン機能をサポートさせる場面などで起こります。

プラグインとして外部のアセンブリからやってくる型の中には、実行時になるまでどんな型なのか分からないものが含まれている場合があります。きっと特定のインターフェースを実装していることくらいはコンパイル時に分かっているでしょうが、肝心の具体的な型が定まっていないはずです。

Imfactは型どうしの依存関係をコンパイル時に探索するので、実行時まで決まらない型の依存関係を解決することはできません。 そもそも実行時まで存在しない型なので、コードに書いたらコンパイラに止められてしまいますから当然なのですけれどね……。こういった場合は、GenericHostなどの動的なDIコンテナを使いましょう。

ちなみに、Imfactで生成されるファクトリークラスはただのクラスなので、ファクトリークラス自体を他のDIコンテナに登録して生成させることもできます。また、ファクトリークラスにファクトリークラスを生成させることもできます。この辺りもImfactの柔軟さに一役買っています。

内部実装の話

内部実装の話を少しだけ……

Imfactでは、生成元のコードのコンパイル結果から情報を抽出して、そこからファクトリークラスの定義を生成しています。抽出と生成の過程は以下のようにいくつかのステップに分かれています。

おわりに

実は以前、Analyzer+CodeFixでもって静的なDIを行うライブラリ「Deptorygen」をリリースしたことがあったのですが、ぼくの環境以外でちゃんと動いていない疑惑があるのと、最近になってついにSourceGeneratorが使えるようになったので、新バージョンとしてImfactが誕生したのでした。

感想などをTwitterでつぶやいてもらえると非常にうれしいので、よろしくお願いします。不具合やご意見などあればGitHubのissueまたはDiscussionへどうぞ。

ちなみに、Imfactという命名は Injection + Factory から取っています。InFactだとあまりにも日常的な言い回しっぽいので、nをmにすり替えています。インパクトのある名前になりましたね!