1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Lazy<T>でDependency Injectionを遅延評価してサービスロケーターパターンを避ける

Last updated at Posted at 2021-10-05

【追記】
その後、Lazy Dependency Injectionについて再度調べたところ、これは「leaky abstraction(漏れやすい抽象化)」であり、依存性注入の法則に反している、という考え方が海外では主流のようです。

漏れやすい抽象化とはつまり、「そのクラスの生成コストが高い為に内部で遅延評価をしている」という、クラス内部の事情が、そのクラスの利用者に漏れ出しているということです。クラスの利用者にとってはそんな事情は本来、意識すべきではないのに、利用者がLazyを渡さなくてはならないのは、抽象化が完全ではありません。

また、同時に「早すぎる最適化」であるという意見もあります。

https://stackoverflow.com/questions/12284487/lazy-dependency-injection

Lazy Dependency Injection を必要とする人が考えるのは、「不要なインスタンスを生成するコストが高い」ということでしょう。私自身もそうでした。
しかし、Lazy Dependency Injectionに反対する人々の考えでは、そもそも「コンストラクタの実行コストが高い」という状態が良くない、ということのようです。
コンストラクタは最低限の事だけをすべきであり、コストのかかる処理はコンストラクタでは行わなず、実際に利用されるタイミングで行うべき、と考えると、確かにlazy Dependency Injectionは不要です。

そういう考え方で作られたオブジェクトを注入するのであれば、確かに使う側は、それを実際に使うかどうか考えずとも気軽に注入し、プライベートフィールドに格納しておけばよいわけです。

また、どうしてもコンストラクタでコストのかかる処理が必要になる場合には、Lazy Dependency Injectionではなく、Proxy Patternを使った方が良いという指摘もありました。

私自身も、今はこれらの考え方に賛同しています。

ただ、Lazy Dependency Injectionが常にダメな手法というわけではなく、どんな場合にも適材適所はあります。
例えば、Proxy Patternを作るほどの手間をかけるべき場所ではないがパフォーマンス改善が必要、という場面で、局所的に使ったりするのはアリでしょう。

手法の一つとして知っておき、同時に「抽象化という理念からは外れる」というデメリットを知っておけば、何らかのシーンで活躍することもあるかもしれません。

課題

DI(Dependency Injection/依存性の注入)前提でシステムを作りこんでいると直面する問題の一つに、注入はするものの、条件によっては使用しないオブジェクトをどうするかという問題がある。

必要になるかもしれない全てのインスタンスを注入すれば良いならそれでよいが、使わないかもしれないものまで事前に用意するというのはパフォーマンス上の問題がある。

この問題の解決方法の一つにサービスロケーターパターンがあるが、これは注入される側のクラスがDIコンテナに依存してしまったり、どんなインスタンスでも呼び出せてしまうグローバルリポジトリを渡すことになってしまう為、やってはいけないアンチパターンである。

解決策

遅延評価の考え方を用いる。

つまりインスタンス自体を渡すのではなく、ファクトリを渡すようにして、必要になったらファクトリ経由でインスタンスを取得するのである。

サービスロケーターとは異なり、あくまでもそのサービスの型に付随するファクトリにのみ依存するので、注入される側のクラスの独立性は保たれたままとなる。

ファクトリパターンを自前で実装しても良いが、遅延評価の為のファクトリパターンを実装する為の標準クラスとして、C#にはLazy<T>がある。

Lazy<T>は、以下のように使う。

Lazyの動作確認
	var lazyString = new Lazy<string>(() => get1000000MBytesString());
	
	Console.Write(lazyString.Value); // Valueを参照した時点でget1000000MBytesStringが実行される。
	
	Console.Write(lazyString.Value); // 2回目の呼び出しではget1000000MBytesStringは呼ばれず結果だけを取得する。

Lazy<T>を使うと、サービスの遅延解決を以下のように書くことができる。
これを Lazy Dependency Injectionと呼ぶ。

以下は、マイクロソフトの標準DIコンテナMicrosoft.Extensions.DependencyInjection を使用してLazy<T>による遅延解決を行うサンプルである。

サービスの遅延評価登録
	services.AddScoped<IMyService, MyService>();
	services.AddScoped<Lazy<IMyService>>(
		sp => new Lazy<IMyService>(
			() => sp.GetService<IMyService>()));
注入される側
class Test
{
	private Lazy<IMyService> _lazyMyService;
	public Test(Lazy<IMyService> lazyMyService)
	{
		_lazyMyService = lazyMyService;
	}
	
	public void DoTest()
	{
		_lazyMyService.Value.DoTest();
	}
}

Lazy.Value はシングルトンパターンになっている為、パフォーマンス的にも問題ない。必要なタイミングでサービスが取得され、2回目以降は再利用される。必要にならなければサービスの取得も発生しない。

また、ドキュメントによればValueプロパティはスレッドセーフなので、AddSingletonしても安心である(もちろん、最終的に取得するT型自体もスレッドセーフにしておかないといけないが…)。

全ての登録済サービスをLazyで受け取れるようにする

とはいえ、前述のやり方だと、遅延解決したいサービスを一つ一つLazy型で登録していかねばならず、少々めんどくさい。

しかし、stackoverflowに素晴らしい提案があった。

services.AddTransient(typeof(Lazy<>), typeof(Lazier<>));

internal class Lazier<T> : Lazy<T> where T : class
{
    public Lazier(IServiceProvider provider)
        : base(() => provider.GetRequiredService<T>())
    {
    }
}

これには唸らされた。ジェネリック型のサービス解決の仕組みを利用して、一度上記のAddTransientを呼び出せば、全ての登録済のT型のサービスをLazy<T>で受け取れるようになる。

少し使いやすくしたVB.NET版もここに置いておく。

Imports Microsoft.Extensions.DependencyInjection
Public Module ServiceCollectionLazyExtensions

    Private Class Lazier(Of T As Class)
        Inherits Lazy(Of T)

        Public Sub New(provider As IServiceProvider)
            MyBase.New(Function() provider.GetRequiredService(Of T)())
        End Sub

    End Class

    <System.Runtime.CompilerServices.Extension>
    Public Function EnableLazyServices(services As IServiceCollection)
        Return services.AddTransient(GetType(System.Lazy(Of)), GetType(Lazier(Of)))
    End Function
    
End Module

ConfigureServicesで次のように呼び出せば、あとはLazy(Of T)で受け取り放題だ。

services.EnableLazyServices()

言語仕様として遅延評価が組み込まれていたらいいのに

上記の方法はシンプルで便利ではあるものの、ソースコードがLazyだらけになるという欠点がある。

もしこんな風に書けたらいいのに、と思わないこともない。

こんな風に書けたらいいのに
class Test
{
	[Lazy]
	private IMyService _myService;
	
	public Test([Lazy] IMyService myService)
	{
		_myService = myService;
	}
	
	public void DoTest()
	{
		// この部分が実際には _myService.Value.DoTest(); としてコンパイルされる。
		_myService.DoTest();  
	}
}

遅延評価を意識しないコーディング

ただ、近いことはコーディング上の工夫で可能ではある。

遅延評価を意識させないコーディング
class Test
{
	private Lazy<IMyService> _lazyMyService;

	private IMyService _myService => _lazyMyService.Value;
	
	public Test(Lazy<IMyService> lazyMyService)
	{
		_lazyMyService = lazyMyService;
	}
	
	public void DoTest()
	{
		_myService.DoTest();  
	}
}

上記のようにしておけば、コンストラクタとメンバを書き変えるだけで、遅延評価の有無を切り替えることが可能になる。

こんなものもある: LazyProxy

検索していたらこんなものも見つけてしまった。

このように使う。

var lazyProxy = LazyProxyBuilder.CreateInstance<IMyService>(() =>
{
    Console.WriteLine("Creating an instance of the real service...");
    return new MyService();
});

なんと、IMyServiceインタフェースを実装して、メソッドが呼ばれた時にLazyのValueを経由してMyServiceへ全ての処理を委譲してくれるプロキシクラスを内部的に自動生成して返してくれるというものだ。

これを使えば、依存性注入される側のクラスは、もはやそれが遅延評価されるものなのかを一切気にする必要がなくなる。

与えられるのはMyServiceであるが、内部では遅延評価されている、ということだ。

非常に便利な気もするが、思わぬ不具合に遭遇した時にとんでもないことになりそうなので、個人的には採用を見送りたい。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?