依存性注入ライフサイクルの理解:シングルトン、スコープ、トランジェントの詳細な例

開発者は依存注入を使用する際に、サービスインスタンスの生命周期を認識することができますが、多くの人はその仕組みを完全に理解していません。オンライン上にはこれらの概念を明確にする多くの記事がありますが、それらは多くは既に知っている可能性の高い定義を再述说しているだけです。私はそれらを簡素化した詳細な例を用いて説明をしていきましょう。

依存注入を実装する際に、開発者は3つの選択肢があり、それぞれインスタンスの生命周期を決定します。

  1. シングルトン
  2. スコープ
  3. トランザイン

多くの開発者はこれらの用語を認識していますが、サービスの生存期間に何を選ぶかを決定する際には多くの人が苦労しています。

定義

定義を始めます:

  • シングルトンの生存期間のサービスインスタンスは、サービスコンテナから1度だけアプリケーションごとに作成されます。1つのインスタンスがその後のすべてのリクエストに対応します。シングルトンサービスはアプリケーションの終了時(つまりアプリケーションの再起動時)に破棄されます。
  • トランザインの生存期間のサービスインスタンスは、リクエストごとにサービスコンテナから作成されます。トランザインサービスはリクエストの終了時に破棄されます。
  • スコープの生存期間のサービスインスタンスは、クライアントのリクエストごとに1度作成されます。トランザインサービスはリクエストの終了時に破棄されます。

使用する際に

  • シングルトン – アプリケーションの生存周期中にサービスの単一インスタンスを使用したい場合
  • 一時的 – クライアント要求内でサービスの個別のインスタンスを使用したい場合
  • スコープ付 – 各要求に対してサービスの単一インスタンスを使用したい場合

クライアント要求とは何か? 非常に簡単に言えば、ユーザーのボタンクリックによってアプリケーションに到着し、レスポンスを取得するAPI/REST呼び出しと考えてください。

心配しないで、例を通じて理解しましょう。

まず、インターフェース/サービスとクラスを作成します:

C#

 

	// 以下のように3つのサービスを宣言しています
		Public interface ISingleton
		Public interface ITransient 
		Public interface IScoped 

次に、上で作成した各サービスの実装を書きます。 callMeSingletoncallMeTransient、およびcallMeScoped変数を更新しようとして、コンセプトを理解しようとします。

  • シングルトンクラスの実装:
C#

 

class SingletonImplementation: ISingleton
{
	var callMeSingleton = ""

	// 他の実装
	public SetSingleton(string value)
	{
		callMeSingleton = value;
	}
	// 他の実装
}

  • 一時的クラスの実装:
C#

 

class TransientImplementation: ITransient 
{
	var callMeTransient = ""
	
	// 他の実装
	public SetTransient(string value)
	{
		callMeTransient = value;
	}
	// 他の実装
}

  • スコープ付きクラスの実装:
C#

 

class ScopedImplementation: IScoped 
{
	var callMeScoped = ""
			
	// 他の実装
	public SetScoped(string value)
	{
		callMeScoped = value;
	}
	// 他の実装		
}

DI (依存性注入) で各サービスインスタンスの生存周期を決定するためにConfigureServicesで登録しましょう。

C#

 

services.AddSingleton<ISingleton, SingletonImplementation>();
services.AddTransient<ITransient , TransientImplementation>();
services.AddScoped<IScoped , ScopedImplementation>();

これらのサービスを3つの異なるクラス(ClassAClassB、およびClassC)から利用/呼び出して、各サービスのライフサイクルを理解しましょう:

  • ClassA:
C#

 

public class ClassA
{
	private ISingleton _singleton;
	// 3つの異なるサービスをインスタンス化するコンストラクタ
	public ClassA(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassA()
	{
		_singleton.SetSingleton("I am from ClassA");
	} 
	
  	public void UpdateTransientFromClassA()
    {
		_transient.SetTransient("I am from ClassA");
	} 
	
  	public void UpdateScopedFromClassA()
	{
		_scoped.SetScoped("I am from ClassA");
	}

	// 他の実装 
}

  • ClassB:
C#

 

public class ClassB
{
	private ISingleton _singleton;
	// 3つの異なるサービスをインスタンス化するコンストラクタ
	public ClassB(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassB()
	{
		_singleton.SetSingleton("I am from ClassB");
	} 
	
  	public void UpdateTransientFromClassB()
    {
		_transient.SetTransient("I am from ClassB");
	} 
	
  	public void UpdateScopedFromClassB()
	{
		_scoped.SetScoped("I am from ClassB");
	}

	// 他の実装 
}

  • ClassC:
C#

 

public class ClassC
{
	private ISingleton _singleton;
	// 3つの異なるサービスをインスタンス化するコンストラクタ
	public ClassC(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassC()
	{
		_singleton.SetSingleton("I am from ClassC");
	} 
	
  	public void UpdateTransientFromClassC()
    {
		_transient.SetTransient("I am from ClassC");
	} 
	
  	public void UpdateScopedFromClassC()
	{
		_scoped.SetScoped("I am from ClassC");
	}

	// 他の実装 
}

分析

上記の実装から、各ライフサイクルの結果と挙動を1つずつ分析しましょう:

シングルトン

全てのクラス(ClassAClassB、およびClassC)は、アプリケーションのライフサイクル全体でSingletonImplementationクラスの同じ単一のインスタンスを使用します。これは、SingletonImplementationクラスのプロパティ、フィールド、操作が全ての呼び出しクラスで使用されるインスタンス間で共有されることを意味します。プロパティやフィールドへの更新は、以前の変更を上書きします。

たとえば、上のコードでは、ClassAClassBClassC はすべて SingletonImplementation サービスを单体インスタンスとして利用し、SetSingleton を呼び出して callMeSingleton 変数を更新します。この場合、すべてのこのプロパティにアクセスを試みるリクエストに共通のcallMeSingleton変数の値があります。最後にアクセスして更新するクラスがcallMeSingletonの値を上書きします。

  • ClassATransientImplementation サービスの場合、他のクラスと同じインスタンスを持つことになります。
  • ClassBTransientImplementation サービスの場合、他のクラスと同じインスタンスを持つことになります。
  • ClassCTransientImplementation サービスの場合、他のクラスと同じインスタンスを持つことになります。

ClassAClassBClassC はすべて SingletonImplementation クラスの同じインスタンスを更新し、これによりcallMeSingletonの値が上書きされます。したがって、单体サービスの実装においてプロパティの設定や更新を行う際には注意を払う必要があります。

单体サービスはアプリケーションの最後に破棄されます(つまり、アプリケーションの再起動時)。

Transient

すべてのクラス(ClassAClassBClassC)はTransientImplementationクラスの個別のインスタンスを使用します。これは、1つのクラスがTransientImplementationクラスのプロパティ、フィールド、または操作を呼び出すと、それはただその個別のインスタンスの値を更新または上書きすることを意味します。TransientImplementationの他のインスタンス間でプロパティやフィールドの更新は共有されません。

理解してください:

  • ClassATransientImplementationのサービスの個別のインスタンスを持つことになります。
  • ClassBTransientImplementationのサービスの個別のインスタンスを持つことになります。
  • ClassCTransientImplementationのサービスの個別のインスタンスを持つことになります。

假にClassDClassAClassBClassCの各インスタンスから transient サービスを呼び出しているとします。この場合、各クラスのインスタンスは異なる/別のインスタンスとして处理され、各クラスにはcallMeTransientの独自の値があります。下のインラインコメントを読むことによってClassDを理解してください。

C#

 

public ClassD
{
	// 他の実装
		
    // 以下のコードで、`callMeTransient` の値を `"I am from ClassA"` に更新し、これは `ClassA` のインスタンスにのみ適用されます。
    // そして、これは `ClassB` または `ClassB` からの次の呼び出しによって変更されません。
	ClassA.UpdateTransientFromClassA(); 		
       
    // 以下のコードで、`callMeTransient` の値を `"I am from ClassB"` に更新し、これは `ClassB` のインスタンスにのみ適用されます。
    // これは `ClassA` のインスタンスの値を上書きしないし、`ClassC` からの次の呼び出しによって変更されることもありません。
	ClassB.UpdateTransientFromClassB(); 
    
    // 以下のコードで、`callMeTransient` の値を `"I am from ClassC"` に更新し、これは `ClassC` のインスタンスにのみ適用されます。
    // これは `ClassA` や `ClassB` のインスタンスの値を上書きしないし、他のクラスからの次の呼び出しによって変更されることもありません。
    ClassC.UpdateTransientFromClassC(); 

	// 他の実装
}

非持続的なサービスはそれぞれのリクエストの終了時に破棄されます。非持続的な動作をリクエスト内で実現したい場合に非持続的な状態を使用してください。

スコープ

すべてのクラス(`ClassA`、`ClassB`、`ClassC`)は各リクエストごとに `ScopedImplementation` クラスの单一のインスタンスを使用します。これは `ScopedImplementation` クラスのプロパティーやフィールド、操作の呼び出しがリクエストの範囲内の单一のインスタンスで発生します。プロパティーやフィールドの更新は他のクラスと共有されます。

理解しましょう:

  • ClassA – これは `TransientImplementation` のサービスのインスタンスを持っています。
  • ClassBClassAと同じTransientImplementationのインスタンスのサービスを持つ。
  • ClassCClassAおよびClassBに同様のTransientImplementationのインスタンスのサービスを持つ。

假にClassDClassAClassB、およびClassCのインスタンスからのスコープサービスを呼び出しているとします。この場合、各クラスはScopedImplementationクラスの单一のインスタンスを持つことになります。ClassDについてのインラインコメントを下記に読むこと。

C#

 

public class ClassD
{
  // 他の実装

 
  
// 以下のコードは、ClassAのインスタンスにてcallMeScopedの値を"I am from ClassA"に更新します。

  
// しかし、これはスコープ生命周期なので、单例のスコープ実装を保持しています。

  
// そして、これは、ClassBまたはClassCからの次の呼び出しにより上書きされることができます。

	ClassA.UpdateScopedFromClassA();  
  
  
// 以下のコードは、单例のスコープ実装にてcallMeScopedの値を"I am from ClassB"に更新します。
 
  
// そして、これはClassAのインスタンスのcallMeScopedの値も上書きします。
 
	ClassB.UpdateScopedFromClassB();
  
  
// Class Aがスコープ実装に何らかの操作を行う場合、

  
// それはclassBによって上書きされた最新のプロパティ/フィールド値を使用します。

	
  
// 以下のコードは、callMeScopedの値を"I am from ClassC"に更新します。

  
// そして、これはClassAやClassBのインスタンスのcallMeScopedの値も上書きします。

	ClassC.UpdateScopedFromClassC(); 
  
// Class BやClass Aがスコープ実装に何らかの操作を行う場合、


    // 他の実装

}

// 他の実装

// スコープサービスは各リクエストの終了時に破棄されます。個別のリクエスト間で状態を持たない動作を望む場合にスコープを使用してください。

// 趣味の時間

// サービスの生命周期は、初期化される親サービスによって上書きできます。混乱していますか?私が説明しましょう。

同じ上の例のクラスから、SingletonImplementation(单一のインスタンス)からTransientScopedサービスを初期化します。これにより、ITransientIScopedサービスを開始し、これらの生命周期を親サービスの单一の生命周期に上書きします。この場合、アプリケーションにはTransientまたはScopedサービスが存在しないことになります(私たちが例を使っている3つのサービスを考慮してください)。

以下のコードの行を読むと、

C#

 

public class SingletonImplementation: ISingleton
{
	// サービスの初期化を行うコンストラクタ
	private readonly ITransient _transient 
	private readonly IScoped _scoped 
		
	SingletonImplementation(ITransient transient, IScoped scoped)
	{
		_transient = transient;  
        // 今のところ_transientは、そのままのTransientとして定義されているとしても、singletonサービスのように振る舞います
		_scoped = scoped;
        // 今のところ、scopedは、Scopedとして登録されているとしても、singletonサービスのように振る舞います
	}
    var callMeSingleton = ""
		
	// 他の実装
}

概要

上記の記事が、このトピックを理解する上で役立つことを期待してください。上記のコンテキストで試してみてください。これにより、これからまたは二度と混乱しないでください。Singletonは、最も簡単に理解できます。なぜなら、一度インスタンスを作成すると、それはアプリケーションの生命周期の中で共有されるからです。Singletonに似たように、Scopedのインスタンスは、要求の生命周期の中で同様の動作をしますが、アプリケーションを跨越しません。Transientは完全にステートレスです。各要求において、各クラスインスタンスはサービスの独自のインスタンスを保持します。

Source:
https://dzone.com/articles/understanding-the-dependency-injection-lifecycle