Окружающий контекст (Ambient Context)

Как мы можем сделать зависимость доступной для каждого модуля, не загрязняя каждый API Cross-Cutting Concerns?

Делая ее пригодной для использования через статический доступ.

Рисунок 4-8: Каждый модуль при надобности может получить доступ к Ambient Context

По-настоящему универсальный Cross-Cutting Concern потенциально может загрязнить большую часть API для приложения, если вам нужно передать экземпляр каждому элементу. В качестве альтернативы можно определить контекст, доступный всем, кто в нем нуждается, и который может быть проигнорирован всеми остальными.

Как это работает

Ambient Context (окружающий контекст) доступен любому потребителю через статическое свойство или метод. Потребляющий класс может использовать его так:

public string GetMessage()
{
	return SomeContext.Current.SomeValue;
}

В данном случае контекст имеет статическое свойство Current, к которому потребитель может получить доступ. Это свойство может быть по-настоящему статическим или может быть связано с выполняемым в данный момент потоком.

Чтобы быть полезным в DI сценариях, контекст сам по себе должен быть абстракцией и должна иметься возможность менять контекст извне: для предыдущего примера это обозначает, что свойство Current должно быть доступно для записи. Контекст сам по себе может быть реализован так, как показано в следующем листинге.

Листинг 4-11: Ambient Context
public abstract class SomeContext
{
	public static SomeContext Current
	{
		get
		{
			var ctx =
				Thread.GetData(
					Thread.GetNamedDataSlot("SomeContext"))
				as SomeContext;
			if (ctx == null)
			{
				ctx = SomeContext.Default;
				Thread.SetData(
					Thread.GetNamedDataSlot("SomeContext"),
					ctx);
			}
			return ctx;
		}
		set
		{
			Thread.SetData(
				Thread.GetNamedDataSlot("SomeContext"),
				value);
		}
	}
	public static SomeContext Default =
		new DefaultContext();
	public abstract string SomeValue { get; }
}

Строки 7-10: Получить текущий контекст из TLS

Строки 22-24: Сохранить текущий контекст в TLS

Строка 29: Значение, переносимое контекстом

Контекст является абстрактным классом, который позволяет заменить один контекст другой реализацией во время выполнения.

В данном примере свойство Current сохраняет текущий контекст в Локальном Хранилище Потока (Thread Local Storage (TLS)), что обозначает, что каждый поток имеет свой собственный контекст, который независим от контекста любого другого потока. В случаях, когда для TLS не был присвоен контекст, возвращается реализация по умолчанию. Важно иметь возможность гарантировать, что ни один потребитель не получит NullReferenceException, когда он попытается получить доступ к свойству Current, поэтому нужно иметь хорошую Local Default. Отметим, что в этом случае свойство Default распределяется по всем потокам. Это работает, потому что в данном примере DefaultContext (класс, который наследуется от SomeContext) является неизменным. Если контекст по умолчанию изменяемый, вам нужно будет назначить отдельный экземпляр для каждого потока, чтобы предотвратить перекрестное загрязнение потоков.

Внешние клиенты могут назначить новый контекст для TLS. Обратите внимание, что возможно присвоить null, но если это произойдет, то следующая операция чтения автоматически переназначит контекст по умолчанию.

Весь смысл использования Ambient Context заключается во взаимодействии с ним. В данном примере это взаимодействие представлено одиночным абстрактным строковым свойством, но контекстный класс может быть и простым, и сложным, когда это необходимо.

Внимание

Для простоты я слегка пропустил безопасность потоков в коде в листинге 4-11. Если вы решили реализовать основанный на TLS Ambient Context, убедитесь, что вы знаете, что делаете.

Совет

Пример в листинге 4-11 использует TLS, но вы также можете использовать CallContext для получения подобного результата.

Примечание

Ambient Context не обязательно должен быть связан с потоком или вызываемым контекстом. Иногда имеет больше смысла сделать так, чтобы он применялся ко всему AppDomain, указав его как static.

Если вы хотите заменить контекст по умолчанию пользовательским контекстом, вы можете создать пользовательскую реализацию, которая наследуется от контекста, и назначить ее в нужное время:

SomeContext.Current = new MyContext();

Для контекста на основе TLS вы должны присвоить пользовательский экземпляр, когда вы создаете новый поток, в то время как по-настоящему универсальный контекст можно назначить в Composition Root.

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

Ambient Context должен быть использован только в редчайших случаях. Для большинства случаев больше подходят внедрение в конструктор или внедрение в свойство, но у вас может быть реальный Cross-Cutting Concern, который загрязняет каждый API в вашем приложении, если вам нужно передать его всем сервисам.

Внимание

Ambient Context сходный по структуре с анти-паттерном Service Locator, который я опишу в главе 5. Разница состоит в том, что Ambient Context предоставляет экземпляр только одной, строго типизированной зависимости, в то время как Service Locator предположительно должен обеспечить экземпляры для каждой зависимости, которую вы можете запросить. Различия являются тонкими, так что убедитесь, что вы полностью понимаете, когда следует применять Ambient Context, прежде чем сделать это. Если вы сомневаетесь, выберите другой DI паттерн.

В разделе 4.4.4, что я реализую TimeProvider, который может быть использован, чтобы получить текущее время, и я также объясню, почему я предпочитаю его статическим членам DateTime. Текущее время является настоящим Cross-Cutting Concern, потому что вы не можете предсказать, каким классам в каких слоях оно может понадобиться. Большинство классов, вероятно, могут использовать текущее время, но лишь небольшая часть из них собираются это сделать.

Потенциально это может заставить вас писать много кода с дополнительным параметром TimeProvider, потому что вы не знаете, когда он сможет вам понадобиться:

public string GetSomething(SomeService service,
	TimeProvider timeProvider)
{
	return service.GetStuff("Foo", timeProvider);
}

Предыдущий метод передает параметр TimeProvider для этого сервиса. Это может выглядеть безобидно, но когда мы затем просмотрим метод GetStuff, мы обнаружим, что это никогда не используется:

public string GetStuff(string s, TimeProvider timeProvider)
{
	return this.Stuff(s);
}

В данном случае параметр TimeProvider передается в качестве дополнительного багажа только потому, что он может однажды понадобиться. Это загрязняет API ненужными CCC, и код становится дурнопахнущим.

Ambient Context может быть решением этой проблемы, если встретятся условия, описанные в таблице 4-4.

Таблица 4-4: Условия для реализации Ambient Context
Условие Описание
Вам нужно, чтобы контекст был запрашиваемым. Если вам нужно только записать некоторые данные (все методы для контекста возвращают void), перехват (Interception) является лучшим решением. Может показаться, что это редкий случай, но он довольно частый: создать лог, что что-то случилось, записать метрики производительности, доказать, что контекст безопасности не подвержен риску – все подобные действия являются чистыми утверждениями (Assertion), которые лучше моделируются при помощи перехвата. Вы должны только рассмотреть возможность использования Ambient Context, если необходимо запросить его для некоторого значения (например, текущего времени).
Существует хорошая Local Default. Существование Ambient Context является неявным (подробнее об этом далее), поэтому очень важно, чтобы контекст просто работал, даже в тех случаях, когда он не назначен явно.
Он должен быть гарантированно доступен. Даже при надлежащей Local Default, важно сделать так, чтобы не было возможности присвоить null, что сделает контекст недоступным и все клиенты выбросят NullReferenceExceptions. Листинг 4-11 показывает некоторые из шагов, которые можно предпринять, чтобы обеспечить это.

В большинстве случаев, преимущества Ambient Context не оправдывают недостатки, поэтому убедитесь, что вы можете удовлетворить всем этим условиям, а если не можете, рассмотрите другие альтернативы.

Таблица 4-5: Преимущества и недостатки Ambient Context
Преимущества Недостатки
Не засоряет API Неявный
Всегда доступен Тяжело корректно реализовать
Может неправильно работать в конкретных рантаймах

Фактически самым большим недостатком Ambient Context является его имплицитность, но, как видно из листинга 4-11, может также быть трудно реализовать его правильно и могут возникнуть проблемы с некоторыми средами исполнения (ASP.NET).

В следующих разделах мы более детально рассмотрим каждый из недостатков, которые описаны в таблице 4-5.

Неявность

Рассмотрим класс, показанный на рисунке 4-9: он не проявляет никаких внешних признаков использования Ambient Context, а метод GetMessage реализуется следующим образом:

public string GetMessage()
{
	return SomeContext.Current.SomeValue;
}
Рисунок 4-9: Класс и его метод GetMessage не проявляют внешних признаков использования Ambient Context, но это вполне может быть так.

Когда Ambient Context правильно реализован, вы можете, по крайней мере, ожидать, что не будет выброшено никаких исключений, но в этом примере контекст влияет на поведение метода, поскольку он определяет возвращаемое значение. Если контекст изменится, может поведение измениться, и вы сразу и не поймете, почему это произошло.

Примечание

В Domain-Driven Design, Эрик Эванс обсуждает Intention-Revealing Interfaces (Evans, Domain-Driven Design, 246), где речь идет о том, что API должен коммуницировать, что он делает только при помощи своего открытого интерфейса. Когда класс использует Ambient Context, он делает с точностью до наоборот: ваш шанс узнать, что имеет место быть тот самый случай, заключается только в чтении документации или просмотре кода.

Помимо потенциальных тонких ошибок это неявность также затрудняет обнаружение точки расширяемости класса. Ambient Context позволяет внедрить пользовательское поведение в любой класс, который использует его, но не очевидно, что это может быть так. Вы можете обнаружить это, читая документацию или понимания реализацию гораздо более подробно, чем вы могли бы хотеть.

Запутанная реализация

Правильная реализация Ambient Context может оказаться непростой задачей. По крайней мере, вы должны гарантировать, что контекст находится всегда в последовательном состоянии, то есть он не должен выбрасывать любые NullReferenceExceptions только потому, что одна реализация контекста была удалена без замены на другую.

Чтобы убедиться в этом, вам необходимо иметь подходящую Local Default, которая может быть использована, если никакая другая реализация не была явно определена. В листинге 4-11 я использовал отложенную инициализацию свойства Current, потому что C# не включает потоко-статические инициализаторы.

Когда Ambient Context представляет собой поистине универсальную концепцию, вы можете получить это при помощи простого записываемого Одиночка (Singleton): один экземпляр, который распространяется по всему AppDomain. Я покажу вам пример этого далее.

Ambient Context может также представлять контекст, который варьируется в зависимости от контекста в стеке вызовов, например от того, кто инициировал запрос. Мы видим это часто в веб-приложениях и веб-сервисах, где тот же самый код выполняется в контексте нескольких пользователей – и каждый на своем собственном потоке. В этом случае Ambient Context может иметь сходство с выполняемым в данный момент потоком и храниться в TLS, как мы видели в листинге 4-11, но это приводит к другим вопросам, в частности для ASP.NET.

Проблемы с ASP.NET

Когда Ambient Context использует TLS, могут возникать проблемы с ASP.NET, потому что он может менять потоки в определенные моменты жизненного цикла страницы, и нет никакой гарантии, что все, что хранится в TLS, будет скопировано из старого потока в новый.

Если такое случается, то вместо TLS вы должны использовать текущий HttpContext для хранения специфичных для запроса данных.

Это поведение по переключению потоков не является проблемой, если Ambient Context – это универсально распространяющийся экземпляр, потому что Singleton является общим для всех потоков в AppDomain.

Использование

.NET BCL содержит несколько реализаций Ambient Context.

Безопасность решается при помощи интерфейса System.Security.Principal.IPrincipal, который связан с каждым потоком. Вы можете получить или установить текущей принципал для потока при помощи аксессора Thread.CurrentPrincipal.

Другой Ambient Context на основе TLS моделирует текущую культуру потока. Thread.CurrentCulture и Thread.CurrentUICulture и позволяют получить доступ и изменить язык и региональные параметры текущей операции. Многие форматирующие API, такие как парсинг и преобразование типов значений, неявно используют текущие региональные параметры и язык, если иное не предоставлено явно.

Трассировка является примером универсального Ambient Context. Класс Trace не связан с конкретным потоком, но действительно является общим для всего AppDomain. Вы можете написать сообщение трассировки отовсюду при помощи метода Trace.Write, и оно будет написано для любого количества TraceListeners, которые конфигурируются свойством Trace.Listeners.

Пример: кеширование Currency

Абстракция Currency в примере коммерческого приложения из предыдущих разделов примерно такая же «говорящая», как и интерфейс. Каждый раз, когда вы хотите конвертировать валюту, вы вызываете метод GetExchangeRateFor, который потенциально ищет обменный курс в какой-то внешней системе. Это гибкий API дизайн, потому что вы можете посмотреть курс фактически в режиме реального времени, если вам это нужно, но в большинстве случаев в этом не будет необходимости, и это, скорее всего, может стать узким местом.

Реализация на основе SQL Server, которую я представил в листинге 4-10, конечно, выполняет запрос к базе данных каждый раз, когда вы запрашиваете обменный курсе. Когда приложение отображает покупательскую корзину, каждый элемент в корзине конвертируется, так что это приводит к запросам к базе данных для каждого элемента в корзине, даже если курс не меняется от первого до последнего элемента. Было бы лучше кэшировать обменный курс на некоторое время, чтобы приложению не нужно было стучаться к базе данных по поводу одного и того же обменного курса несколько раз в пределах одной доли секунды.

В зависимости от того, насколько это важно – иметь текущие валюты, время кэша может быть коротким или длинным: кэш для одной секунды или для нескольких часов. Тайм-аут должен быть настраиваемым.

Чтобы определить, когда истекает кэш валюты, вам нужно знать, сколько времени прошло с того момента, когда валюта была закеширована, так что вы должны иметь доступ к текущему времени. DateTime.UtcNow кажется встроенным Ambient Context, но это не так, потому что вы не можете назначить время, только запросить его.

Неспособность переопределить текущее время редко вызывает проблемы в реальном приложении, но может быть проблемой при модульном тестировании.

Моделирование времени

В то время как обычному веб-приложению вряд ли нужна возможность изменять текущее время, другой тип приложений может извлечь большую пользу от этой способности.

Однажды я написал довольно сложный движок моделирования, который зависел от текущего времени. Поскольку я всегда использую Test-Driven Development (TDD), я уже использовал абстракцию текущего времени, так что я мог внедрить экземпляры DateTime, которые отличались от реального машинного времени. Это оказалось огромным преимуществом, когда мне позже понадобилось ускорить время в симуляции на несколько порядков. Все, что я должен был сделать, это зарегистрировать провайдер времени, который ускорял время, и вся симуляция немедленно ускорялась.

Если вы хотите увидеть аналогичный функционал, вы можете посмотреть на клиентское приложение Всемирного телескопа (WorldWide Telescope, http://www.worldwidetelescope.org), которое позволяет моделировать ночное небо в ускоренном времени. На рисунке ниже показан скриншот элемента управления, который позволяет запускать время вперед и назад с различной скоростью. Я понятия не имею, реализовали ли разработчики эту возможность при помощи провайдера времени с Ambient Context, но это то, что сделал бы я.

Всемирный телескоп позволяет поставить время на паузу или промотать его назад и вперед с различной скоростью. Это симулирует вид ночного неба в разное время.

В случае с примером коммерческого приложения, я хочу иметь возможность контролировать время, когда я пишу юнит тесты, чтобы я мог убедиться, что кэш валют истекает правильно.

TimeProvider

Время является довольно универсальной концепцией (даже если время движется с разной скоростью в разных частях Вселенной), поэтому я могу моделировать его как всеобще распространенный ресурс. Поскольку нет никаких причин иметь отдельные провайдеры времени для каждого потока, Ambient Context TimeProvider является записываемым Singleton, как показано в следующем листинге.

Листинг 4-12:TimeProvider Ambient Context
public abstract class TimeProvider
{
	private static TimeProvider current;
	static TimeProvider()
	{
		TimeProvider.current =
			new DefaultTimeProvider();
	}
	public static TimeProvider Current
	{
		get { return TimeProvider.current; }
		set
		{
			if (value == null)
			{
				throw new ArgumentNullException("value");
			}
			TimeProvider.current = value;
		}
	}
	public abstract DateTime UtcNow { get; }
	public static void ResetToDefault()
	{
		TimeProvider.current =
			new DefaultTimeProvider();
	}
}

Строки 6-7: Инициализация TimeProvider по умолчанию

Строки 14-17: Ограждающее условие

Строка 21: Важная часть

Цель класса TimeProvider состоит в том, чтобы вы могли контролировать, как время доводится до клиентов. Как описано в таблице 4-4, Local Default важна, поэтому вы статически инициализируете класс, чтобы использовать класс DefaultTimeProvider (я покажу вам это в ближайшее время).

Еще одно условие из таблицы 4-4 заключается в том, что вы должны гарантировать, что TimeProvider никогда не будет в нестабильном состоянии. Поле current никогда не должно быть null, поэтому ограждающее условие гарантирует, что этого никогда не будет.

Все это основа, чтобы сделать TimeProvider доступным отовсюду. Смыслом его существования является способность обрабатывать экземпляры DateTime, представляющие текущее время. Я целенаправленно смоделировал имя и сигнатуру абстрактного свойства после DateTime.UtcNow. При необходимости я могу также добавили такие абстрактные свойства как Now и Today, но я не нуждаюсь в них для этого примера.

Наличие надлежащей и значимой Local Default является важным, и к счастью, для этого примера это не трудно сделать, потому что она должна просто вернуть текущее время. Это означает, что пока вы явно не войдете и не назначите другой TimeProvider, любой клиент, использующий TimeProvider.Current.UtcNow, получит реальное текущее время.

Реализация DefaultTimeProvider показана в следующем листинге.

Листинг 4-13: Провайдер времени по умолчанию
public class DefaultTimeProvider : TimeProvider
{
	public override DateTime UtcNow
	{
		get { return DateTime.UtcNow; }
	}
}

Класс DefaultTimeProvider наследуется от TimeProvider, чтобы предоставить реальное время каждый раз, когда клиент читает свойство UtcNow.

Когда CachingCurrency использует Ambient Context TimeProvider для получения текущего времени, он получит реальное текущее время, пока вы напрямую не назначите приложению другой TimeProvider; и я планирую сделать это только в моих модульных тестах.

Кэширование валют

Для реализации кэширования валют, нужно реализовать Декоратор (Decorator), который меняет "правильную" реализацию Currency.

Примечание

Паттерн проектирования Декоратор является важной частью перехвата, я буду обсуждать это более подробно в главе 9.

Вместо изменения существующей, поддерживаемой SQL Server реализации Currency, показанной в листинге 4-10, вы просто обернете кэш вокруг нее и только вызовете реальную реализацию, если кэш истек или не содержит записи.

Как вы помните из раздела 4.1.4, CurrencyProvider – это абстрактный класс, который возвращает экземпляры Currency. CachingCurrencyProvider реализует тот же базовый класс и оборачивает функционал содержащегося CurrencyProvider. Всякий раз, когда он запрашивает Currency, он возвращает Currency, созданный содержащимся CurrencyProvider, но обернутый в CachingCurrency (см. рисунок 4-10).

Рисунок 4-10: CachingCurrencyProvider оборачивает "реальный" CurrencyProvider и возвращает экземпляры CachingCurrency, которые оборачивают "реальные" экземпляры Currency.

Такой паттерн позволяет мне кэшировать любую реализацию валюты, а не только реализацию на основе SQL Server, которая есть у меня в настоящее время. Рисунок 4-11 показывает план класса CachingCurrency.

Рисунок 4-11: CachingCurrency принимает в свой конструктор внутреннюю валюту (innerCurrency) и время действия кэша (cacheTimeout) и оборачивает функционал внутренней валюты.

Совет

Паттерн Декоратор является одним из лучших способов реализации разделения ответственности (Separation of Concerns, SoC).

CachingCurrency использует внедрение в конструктор, чтобы получить "реальный" экземпляр, чьи курсы валют он должен хранить в кэше. Например, CachingCurrency делегирует свое свойство Code свойству Code внутреннего Currency.

Интересной частью реализации CachingCurrency является его метод GetExchangeRateFor, показанный в следующем листинге.

Листинг 4-14: Кэширование обменного курса
private readonly Dictionary<string, CurrencyCacheEntry> cache;
public override decimal GetExchangeRateFor(string currencyCode)
{
	CurrencyCacheEntry cacheEntry;
	if ((this.cache.TryGetValue(currencyCode,
			out cacheEntry))
		&& (!cacheEntry.IsExpired))
	{
		return cacheEntry.ExchangeRate;
	}
	var exchangeRate =
		this.innerCurrency
		.GetExchangeRateFor(currencyCode);
	var expiration =
		TimeProvider.Current.UtcNow + this.CacheTimeout;
	this.cache[currencyCode] =
		new CurrencyCacheEntry(exchangeRate, expiration);
	return exchangeRate;
}

Строки 4-10: Вернуть закэшированный обменный курс, если он подходит

Строки 16-17: Сохранить в кэше обменный курс

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

Только если действующего закэшированного обменного курса нет, вы вызываете внутренний Currency, чтобы получить обменный курс. Прежде чем вернуть его, необходимо его кэшировать. Первый шаг состоит в вычислении срока истечения, и тут вы используете TimeProvider Ambient Context, вместо более традиционного DateTime.Now. С вычисленным сроком истечения вы можете кэшировать запись перед возвратом результата.

Вычисление того, истек ли срок действия кэша, также делается при помощи TimeProvider Ambient Context.

return TimeProvider.Current.UtcNow >= this.expiration;

Класс CachingCurrency использует TimeProvider Ambient Context во всех местах, где ему нужно текущее время, так что можно написать модульный тест, который точно контролирует время.

Модификация времени

При модульном тестировании класса CachingCurrency, вы можете точно контролировать время совершенно независимо от часов реальной системы. Это позволяет писать детерминистические модульные тесты, хотя тестируемая система (System Under Test, SUT) зависит от концепции текущего времени. Следующий листинг показывает тест, который проверяет, что хотя SUT запрашивает обменный курс четыре раза, внутренняя валюта вызывается только дважды: при первом запросе и снова, когда истекает время действия кэша.

Листинг 4-15: Юнит тест на предмет того, что валюта корректно кэшируется и что срок действия корректно заканчивается
[Fact]
public void InnerCurrencyIsInvokedAgainWhenCacheExpires()
{
	// Fixture setup
	var currencyCode = "CHF";
	var cacheTimeout = TimeSpan.FromHours(1);
	var startTime = new DateTime(2009, 8, 29);
	var timeProviderStub = new Mock<TimeProvider>();
	timeProviderStub
		.SetupGet(tp => tp.UtcNow)
		.Returns(startTime);
	TimeProvider.Current = timeProviderStub.Object;
	var innerCurrencyMock = new Mock<Currency>();
	innerCurrencyMock
		.Setup(c => c.GetExchangeRateFor(currencyCode))
		.Returns(4.911m)
		.Verifiable();
	var sut =
		new CachingCurrency(innerCurrencyMock.Object,
			cacheTimeout);
	sut.GetExchangeRateFor(currencyCode);
	sut.GetExchangeRateFor(currencyCode);
	sut.GetExchangeRateFor(currencyCode);
	timeProviderStub
		.SetupGet(tp => tp.UtcNow)
		.Returns(startTime + cacheTimeout);
	// Exercise system
	sut.GetExchangeRateFor(currencyCode);
	// Verify outcome
	innerCurrencyMock.Verify(
		c => c.GetExchangeRateFor(currencyCode),
		Times.Exactly(2));
	// Teardown (implicit)
}

Строка 12: Установка TimeProvider Ambient Context

Строка 21: Должна быть вызвана внутренняя валюта

Строки 22-23: Должна быть закэширована

Строки 24-26: Время истечения срока действия

Строка 28: Должна быть вызвана внутренняя валюта

Строки 30-32: Проверка на то, что внутренняя валюта была вызвана правильно

Внимание, жаргон

Следующий текст содержит некоторую терминологию модульного тестирования: я выделил ее курсивом, а поскольку эта книга не о модульном тестировании, я отправляю вас к книге xUnit Test Patterns (Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code (New York: Addison-Wesley, 2007), которая является источником всех этих имен паттернов.

Одна из первых вещей, которую нужно сделать в этом тесте, заключается в создании «дублера для тестирования» (Test Double) для TimeProvider, который будет возвращать экземпляры DateTime, как они определены, вместо того чтобы основываться на часах системы. В этом тесте я использую динамический mock фреймворк, называемый Moq (http://code.google.com/p/moq/), чтобы определить, что свойство UtcNow вернет тот же DateTime, пока не указано иное. Когда все определено, эта заглушка (Stub) внедряется в Ambient Context.

Первый вызов GetExchangeRateFor должен вызвать внутренний CachingCurrency Currency, потому что еще ничего не находится в кэше, а два следующие вызова должны вернуть кэшированное значение, потому что срок еще не истек, в соответствии с заглушкой TimeProvider.

С парой кэшированных вызовов настала пора времени продвинуться вперед, вы меняете заглушку TimeProvider, чтобы вернуть экземпляр DateTime, для которого как раз истек срок действия кэша, и снова вызываете метод GetExchangeRateFor, ожидая, что он вызовет внутренний Currency во второй раз, потому что действительные записи кэша теперь уже истекли.

Поскольку вы ожидаете, что внутренний Currency вызывается дважды, вы, наконец, убеждаетесь в этом, говоря внутреннему Currency Mock, что метод GetExchangeRateFor должен быть вызван ровно два раза.

Одна из многих опасностей Ambient Context заключается в том, что как только он назначен, он остается одинаковым, пока не будут изменен снова, но в связи с его неявной природой, об этом можно легко забыть. В модульном тесте, например, поведение, определенное с помощью теста в листинге 4-15, остается одинаковым, если не будет явного сброса (что я делаю в Fixture Teardown). Это может привести к серьезным ошибкам (на этот раз в моем тестовом коде), потому что это распространится и загрязнит тесты, которые выполняются после этого теста.

Ambient Context выглядит обманчиво простым в реализации и использовании и может привести ко многим трудным для поиска ошибкам. Его можно использовать, но делайте это только тогда, когда не существует лучшей альтернативы. Это как хрен: отлично подходит для определенных вещей, но определенно не универсален.

Связанные паттерны

Ambient Context может быть использован для моделирования Cross-Cutting Concern, хотя и требует, чтобы у нас была подходящая Local Default.

Если окажется, что зависимость вообще не является Cross-Cutting Concern, вы должны изменить DI стратегию. Если у вас есть Local Default, вы можете переключиться на внедрение в свойство, а в противном случае вы должны использовать внедрение в конструктор.

или RSS канал: Что новенького на smarly.net