Понимание жизненного цикла внедрения зависимостей: Singleton, Scoped и Transient с подробными примерами

Разработчики могут быть знакомы с жизненным циклом экземпляров службы при использовании деpendency injection, но многие еще не полностью понимают, как это работает. В интернете можно найти множество статей, которые объясняют这些概念, но они, как правило, просто повторяют определения, которые у вас уже могут быть известны. Permit me to illustrate with a detailed example that simplifies the explanation.

При реализации dependency injection, разработчики имеют три варианта, определяющих жизненный цикл экземпляров:

  1. Singleton
  2. Scoped
  3. Transient

虽然大多数开发者认可这些术语,但仍有大量开发者难以确定服务生命周期的选项。

Определения

Permit me to start with definitions:

  • Singleton жизненный цикл службы экземпляры создаются один раз на протяжении всего приложения из контейнера службы. Единственный экземпляр будет обслуживать все последующие запросы. Singleton services are disposed of at the end of the application (i.e., upon application restart).
  • Transient жизненный цикл службы экземпляры создаются по запросу из контейнера службы. Transient services are disposed of at the end of the request.
  • Scoped жизненный цикл службы экземпляры создаются один раз на протяжении всего приложения из контейнера службы. Transient services are disposed of at the end of the request.

Когда использовать

  • Синглтон – когда вы хотите использовать единственный экземпляр сервисов на протяжении всего срока жизни приложения
  • Переменный – когда вы хотите использовать отдельные экземпляры сервисов в рамках запроса клиента
  • Области – когда вы хотите использовать единственный экземпляр сервиса для каждого запроса

Что такое запрос клиента? Если вы упростите это, можете рассмотреть его как API/REST-запрос, который приходит в ваше приложение от щелчек пользователя кнопок для получения ответа.

Не волнуйтесь, посмотрим пример.

Пример

Сначала создадим интерфейсы/сервисы и классы:

C#

 

	// мы объявляем 3 сервиса ниже
		Public interface ISingleton
		Public interface ITransient 
		Public interface IScoped 

Теперь напишем реализацию для каждого сервисного интерфейса/сервиса, созданного выше. Мы попробуем понять концепцию, изучая попытку обновить переменные callMeSingleton, callMeTransient и 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;
	}
	// другая реализация		
}

Проregistration (ConfigureServices) с DI (Внедрение зависимостей), чтобы решить цикл жизни каждого экземпляра сервиса:

C#

 

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

Давайте используем/вызываем эти услуги из 3-х различных классов (ClassA, ClassB и 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");
	}

	// другие реализации 
}

Анализ

Посмотрим на результаты и поведение для каждого этапа жизненного цикла по-одному, исходя из указанной выше реализации:

Сингleton

Все классы (ClassA, ClassB и ClassC) будут использовать одно и то же единственное экземпляр класса SingletonImplementation на весь период жизни приложения. Это意味着 свойства, поля и операции класса SingletonImplementation будут share между всеми экземплярами, используемыми в вызывающих классах. любые обновления свойств или полей переопределят предыдущие изменения.

К примеру, в коде выше ClassA, ClassB и ClassC все используют сервис SingletonImplementation в качестве единственного экземпляра и вызывают SetSingleton, чтобы обновить переменную callMeSingleton. В этом случае будет единый значение переменной callMeSingleton для всех запросов, пытающихся получить доступ к этому свойству. Класс, который последним обращается к ней для обновления, переопределит значение callMeSingleton.

  • ClassA – У него будет такой же экземпляр, как и у других классов для сервиса TransientImplementation.
  • ClassB – У него будет такой же экземпляр, как и у других классов для сервиса TransientImplementation.
  • ClassC – У него будет такой же экземпляр, как и у других классов для сервиса TransientImplementation.

ClassA, ClassB и ClassC обновляют один и тот же экземпляр класса SingletonImplementation, который переопределит значение callMeSingleton. Таким образом, будьте осторожны, когда устанавливаете или обновляете свойства в реализации единственного экземпляра сервиса.

Единственные экземпляры сервисов удаляются в конце приложения (то есть при перезапуске приложения).

Transient

Все классы (ClassA, ClassB, и ClassC) будут использовать свои индивидуальные экземпляры класса TransientImplementation. Это意味着, если какой-либо класс вызывает свойства, поля или операции класса TransientImplementation, он будет обновлять или переопределять только значения своего индивидуального экземпляра. Любые обновления свойств или полей не разделяются с другими экземплярами TransientImplementation.

Посмотрим так:

  • ClassA – у него будет свой собственный экземпляр службы TransientImplementation.
  • ClassB – у него будет свой собственный экземпляр службы TransientImplementation.
  • ClassC – у него будет свой собственный экземпляр службы TransientImplementation

假设您有一个 ClassD, который вызывает транзиентную службу из инстансов ClassA, ClassB и ClassC. В этом случае каждый экземпляр класса будет рассматриваться как различный/отдельный экземпляр, и каждый класс будет иметь свое собственное значение callMeTransient. Прочитайте комментарии встроенные ниже для ClassD:

C#

 

public ClassD
{
	// другая реализация
		
    // Нижеследующий код обновит значение callMeTransient до "Я от ClassA" только для экземпляра ClassA.
    // А значение не будет изменено ни на следующие вызовах из Class B, ни из Class B
	ClassA.UpdateTransientFromClassA(); 		
       
    // Нижеследующий код обновит значение callMeTransient до "Я от ClassB" только для экземпляра ClassB.
    // И это не переопределит значение для экземпляра ClassA, ни заменит его следующим вызовом из Class C
	ClassB.UpdateTransientFromClassB(); 
    
    // Нижеследующий код обновит значение callMeTransient до "Я от ClassC" только для экземпляра ClassC.
    // И это никогда не переопределит значение для экземпляров ClassA и ClassB, ни изменит его следующим вызовом из других классов.
    ClassC.UpdateTransientFromClassC(); 

	// другая реализация
}

Сервисы Transient удаляются в конце каждого запроса. Используйте Transient, если вы хотите stateless поведение внутри запроса.

Scoped

Все классы (ClassA, ClassB, и ClassC) будут использовать sinльные экземпляры класса ScopedImplementation для каждого запроса. Это意味着 вызовам на свойства/ поля/ операции класса ScopedImplementation происходит на единственном экземпляре в рамках запроса.的任何更新 свойств/полей будут share среди других классов.

Посмотрим на это:

  • ClassA – у него будет свой экземпляр сервиса TransientImplementation.
  • КлассB – У него будет своя собственная инстанция сервиса TransientImplementation, как и у КлассA.
  • КлассC – У него будет своя собственная инстанция сервиса TransientImplementation, как и у КлассA и КлассB.

Предположим, у вас есть КлассD, который вызывает облачный сервис от инстанций КлассA, КлассB и КлассC. В этом случае у каждого класса будет одна иnica инстанция класса ScopedImplementation. Прочитайте комментарии встроенные внизу для КлассD.

C#

 

public class ClassD
{
  // другая реализация
 
  // Нижешь код обновит значение callMeScoped до "Я из ClassA" для экземпляра ClassA
  // Но так как это специфическое жизненное цикловое поведение, оно хранит единственный экземпляр ScopedImplementation
  // Тогда оно может быть изменено следующим вызовом из ClassB или ClassC
	ClassA.UpdateScopedFromClassA();  
  
  // Нижешь код обновит значение callMeScoped до "Я из ClassB" для единственного экземпляра ScopedImplementation 
  // И это также перезаместит значение callMeScoped для экземпляра ClassA. 
	ClassB.UpdateScopedFromClassB();
  
  // Теперь, если Class A будет выполнять какие-либо операции над ScopedImplementation,
  // она будет использовать последние свойства/значения полей, которые были перезаписаны классом B.
	
  // Нижешь код обновит значение callMeScoped до "Я из ClassC"
  // И это также перезаместит значение callMeScoped для экземпляров ClassA и ClassB.
	ClassC.UpdateScopedFromClassC(); 
  // Если Class B или Class A будут выполнять какие-либо операции над ScopedImplementation, они будут использовать последние свойства/значения полей, которые были перезаписаны классом C

    // другая реализация
}

Специфические сервисы уничтожаются в конце каждого запроса. Используйте Специфическое, когда вы хотите стаeless поведение между отдельными запросами.

Тrivia Time

Цикл жизни сервиса может быть изменен родительским сервисом, где он инициализируется. Confused? Let me explain:

Давайте воспользуемся тем же примером, который был ранее указан в классах, и инициализируем не persistentные и scoped сервисы из SingletonImplementation (который является синглтоном) как показано ниже. Это будет инициировать сервисы ITransient и IScoped и перезаписать цикл жизни этих сервисов под цикл жизни синглтона, как родительского сервиса. В этом случае ваша приложение не будет иметь никаких не persistentных или scoped сервисов (considering you just have these 3 services we were using in our examples).

Прочитайте через строки в следующем коде:

C#

 

public class SingletonImplementation: ISingleton
{
	// конструктор для добавления инициализации сервисов. 
	private readonly ITransient _transient 
	private readonly IScoped _scoped 
		
	SingletonImplementation(ITransient transient, IScoped scoped)
	{
		_transient = transient;  
        // Now _transient would behave as singleton service regardless of how it was registered as Transient 
		_scoped = scoped;
        // now scoped would behave as singleton service regardless of it being registered as Scoped 
	}
    var callMeSingleton = ""
		
	// другая реализация 
}

Резюме

Я надеюсь, что предыдущий раздел поможет вам понять тему. Я рекомендую вам попробовать это самому с контекстами, указанными выше, и вы никогда больше не будете путаться. Синглтон самый простой для понимания, потому что как только вы создаете его экземпляр, он будет разделен между приложениями в течение цикла жизни приложения. по аналогии с Синглтоном, scoped экземпляры имитируют то же поведение, но только в течение цикла запроса в приложении. Transient полностью без состояния, для каждого запроса и каждого класса экземпляр будет хранить свой собственный экземпляр сервиса.

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