Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
void), перехват (Int erception) является лучшим решением. Может показаться, что это редкий случай, но он довольно частый: создать лог, что что-то случилось, записать метрики производительности, доказать, что контекст безопасности не подвержен риску – все подобные действия являются чистыми утверждениями (Assertion), которые лучше моделируются при помощи перехвата. Вы должны только рассмотреть возможность использования Am bient Cont ext , если необходимо запросить его для некоторого значения (например, текущего времени). Существует хорошая Local Default. Существование Ambient Context является неявным (подробнее об этом далее), поэтому очень важно, чтобы контекст просто работал, даже в тех случаях, когда он не назначен явно. Он должен быть гарантированно доступен. Даже при надлежащей Local Default, важно сделать так, чтобы не было возможности присвоить null , что сделает контекст недоступным и все клиенты выбросят NullReferenceExceptions Листинг 4-11 показывает некоторые из шагов, которые можно предпринять, чтобы обеспечить это. 148 В большинстве случаев, преимущества Ambient Context не оправдывают недостатки, поэтому убедитесь, что вы можете удовлетворить всем этим условиям, а если не можете, рассмотрите другие альтернативы. Таблица 4-5: Преимущества и недостатки Ambient Context Преимущества Недостатки Не засоряет API Неявный Всегда доступен Т яжело корректно реализовать Может неправильно работать в конкретных рантаймах Фактически самым большим недостатком Ambient Context является его имплицитность, но, как видно из листинга 4-11, может также быть трудно реализовать его правильно и могут возникнуть проблемы с некоторыми средами исполнения (ASP.NET). В следующих разделах мы более детально рассмотрим каждый из недостатков, которые описаны в таблице 4-5. Н еявность Рассмотрим класс, показанный на рисунке 4-9: он не проявляет никаких внешних признаков использования Am bient Context, а метод GetMessage реализуется следующим образом: public string GetMessage() { return SomeContext.Current.SomeValue; } Рисунок 4-9: Класс и его метод GetMessage не проявляют внешних признаков использования Am bient Context, но это вполне может быть так. Когда Ambient Cont ext правильно реализован, вы можете, по крайней мере, ожидать, что не будет выброшено никаких исключений, но в этом примере контекст влияет на поведение метода, поскольку он определяет возвращаемое значение. Если контекст изменится, может поведение измениться, и вы сразу и не поймете, почему это произошло. 149 П римечание В Dom ain-Driven Design, Эрик Эванс обсуждает Intention-Revealing Interfaces (Evans, Domain-Driven Design, 246), где речь идет о том, что API должен коммуницировать, что он делает только при помощи своего открытого интерфейса. Когда класс использует Am bient Context, он делает с точностью до наоборот: ваш шанс узнать, что имеет место быть тот самый случай, заключается только в чтении документации или просмотре кода. Помимо потенциальных тонких ошибок это неявность также затрудняет обнаружение точки расширяемости класса. Am bient Context позволяет внедрить пользовательское поведение в любой класс, который использует его, но не очевидно, что это может быть так. Вы можете обнаружить это, читая документацию или понимания реализацию гораздо более подробно, чем вы могли бы хотеть. Запутанная реализация Правильная реализация Ambient Cont ext может оказаться непростой задачей. По крайней мере, вы должны гарантировать, что контекст находится всегда в последовательно м состоянии, то есть он не должен выбрасывать любые NullReferenceExceptions только потому, что одна реализация контекста была удалена без замены на другую. Чтобы убедиться в этом, вам необходимо иметь подходящую Local Default, которая может быть использована, если никакая другая реализация не была явно определена. В листинге 4-11 я использовал отложенную инициализацию свойства Current , потому что C# не включает потоко-статические инициализатор ы. Когда Ambient Cont ext представляет собой поистине универсальную концепцию, вы можете получить это при помощи простого записываемого Одиночка (Singleton): один экземпляр, который распространяется по всему AppDomain. Я покажу вам пример этого далее. Ambient Context может также представлять контекст, который варьируется в зависимости от контекста в стеке вызовов, например от того, кто инициировал запрос. Мы видим это часто в веб-приложениях и веб-сервисах, где тот же самый код выполняется в контексте нескольких пользователей – и каждый на своем собственном потоке. В этом случае Ambient Context может иметь сходство с выполняемым в данный момент потоком и храниться в TLS, как мы видели в листинге 4-11, но это приводит к другим вопросам, в частности для ASP.NET . П роблемы с ASP.NET Когда Ambient Cont ext использует T LS, могут возникать проблемы с ASP.NET , потому что он может менять потоки в определенные моменты жизненного цикла страницы, и нет никакой гарантии, что все, что хранится в T LS, будет скопировано из старого потока в новый. Если такое случается, то вместо T LS вы должны использовать текущий HttpContext для хранения специфичных для запроса данных. Это поведение по переключению потоков не является проблемой, если Am bient Context – это универсально распространяющийся экземпляр, потому что Singleton является общим для всех потоков в AppDomain 150 Использование .NET BCL содержит несколько реализаций Am bient Context. Безопасность решается при помощи интерфейса System.Security.Principal.IPrincipal , который связан с каждым потоком. Вы можете получить или установить текущей принципал для потока при помощи аксессора Thread.CurrentPrincipal Другой Ambient Context на основе T LS моделирует текущую культуру потока. 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 Cont ext , но это не так, потому что вы не можете назначить время, только запросить его. Неспособность переопределить текущее время редко вызывает проблемы в реальном приложении, но может быть проблемой при модульном тестировании. 151 Моделирование времени В то время как обычному веб-приложению вряд ли нужна возможность изменять текущее время, другой тип приложений может извлечь большую пользу от этой способности. Однажды я написал довольно сложный движок моделирования, который зависел от текущего времени. Поскольку я всегда использую Test-Driven Development (T DD), я уже использовал абстракцию текущего времени, так что я мог внедрить экземпляры DateTime , которые отличались от реального машинного времени. Это оказалось огромным преимуществом, когда мне позже понадобилось ускорить время в симуляции на несколько порядков. Все, что я должен был сделать, это зарегистрировать провайдер времени, который ускорял время, и вся симуляция немедленно ускорялась. Если вы хотите увидеть аналогичный функционал, вы можете посмотреть на клиентское приложение Всемирного телескопа (WorldW ide Telescope, ht tp://www.worldwidetelescope.org ), которое позволяет моделировать ночное небо в ускоренном времени. На рисунке ниже показан скриншот элемента управления, который позволяет запускать время вперед и назад с различной скоростью. Я понятия не имею, реализовали ли разработчики эту возможность при помощи провайдера времени с Am bient Context, но это то, что сделал бы я. Всемирный телескоп позволяет поставить время на паузу или промотать его назад и вперед с различной скоростью. Это симулирует вид ночного неба в разное время. В случае с примером коммерческого приложения, я хочу иметь возможность контролировать время, когда я пишу юнит тесты, чтобы я мог убедиться, что кэш валют истекает правильно. 152 TimeProvider Время является довольно универсальной концепцией (даже если время движется с разной скоростью в разных частях Вселенной), поэтому я могу моделировать его как всеобще распространенный ресурс. Поскольку нет никаких причин иметь отдельные провайдеры времени для каждого потока, Am bient Context TimeProvider является записываемым Singleton, как показано в следующем листинге. Листинг 4-12: TimeProvider Ambient Context 1. public abstract class TimeProvider 2. { 3. private static TimeProvider current; 4. static TimeProvider() 5. { 6. TimeProvider.current = 7. new DefaultTimeProvider(); 8. } 9. public static TimeProvider Current 10. { 11. get { return TimeProvider.current; } 12. set 13. { 14. if (value == null) 15. { 16. throw new ArgumentNullException("value"); 17. } 18. TimeProvider.current = value; 19. } 20. } 21. public abstract DateTime UtcNow { get; } 22. public static void ResetToDefault() 23. { 24. TimeProvider.current = 25. new DefaultTimeProvider(); 26. } 27. } Строки 6-7: Инициализация TimeProvider по умолчанию Строки 14-17: Ограждающее условие Строка 21: Важная часть Цель класса TimeProvider состоит в том, чтобы вы могли контролировать, как время доводится до клиентов. Как описано в таблице 4-4, Local Default важна, поэтому вы статически инициализируете класс, чтобы использовать класс DefaultTimeProvider (я покажу вам это в ближайшее время). Еще одно условие из таблицы 4-4 заключается в том, что вы должны гарантировать, что TimeProvider никогда не будет в нестабильном состоянии. Поле current никогда не должно быть null , поэтому ограждающее условие гарантирует, что этого никогда не будет. Все это основа, чтобы сделать TimeProvider доступным отовсюду. Смыслом его существования является способность обрабатывать экземпляр ы DateTime , представляющие текущее время. Я целенаправленно смоделировал имя и сигнатуру 153 абстрактного свойства после 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). 154 Рисунок 4-10: CachingCurrencyProvider оборачивает "реальный" CurrencyProvider и возвращает экземпляры CachingCurrency , которые оборачивают "реальные" экземпляр ы Currency Такой паттерн позволяет мне кэшировать любую реализацию валюты, а не только реализацию на основе SQL Server, которая есть у меня в настоящее время. Рисунок 4-11 показывает план класса CachingCurrency Рисунок 4-11: CachingCurrency принимает в свой конструктор внутреннюю валюту ( innerCurrency ) и время действия кэша ( cacheTimeout ) и оборачивает функционал внутренней валюты. 155 С овет Паттерн Декоратор является одним из лучших способов реализации разделения от ветственност и (Separat ion of Concerns, SoC). CachingCurrency использует внедрение в конструктор, чтобы получить "реальный" экземпляр, чьи курсы валют он должен хранить в кэше. Например, CachingCurrency делегирует свое свойство Code свойству Code внутреннего Currency Интересной частью реализации CachingCurrency является его метод GetExchangeRateFor , показанный в следующем листинге. Листинг 4-14: Кэширование обменного курса 1. private readonly Dictionary 2. public override decimal GetExchangeRateFor(string currencyCode) 3. { 4. CurrencyCacheEntry cacheEntry; 5. if ((this.cache.TryGetValue(currencyCode, 6. out cacheEntry)) 7. && (!cacheEntry.IsExpired)) 8. { 9. return cacheEntry.ExchangeRate; 10. } 11. var exchangeRate = 12. this.innerCurrency 13. .GetExchangeRateFor(currencyCode); 14. var expiration = 15. TimeProvider.Current.UtcNow + this.CacheTimeout; 16. this.cache[currencyCode] = 17. new CurrencyCacheEntry(exchangeRate, expiration); 18. return exchangeRate; 19. } Строки 4-10: Вернуть закэшированный обменный курс, если он подходит Строки 16-17: Сохранить в кэше обменный курс Когда клиент запрашивает обменный курс, вы сначала перехватываете вызов, чтобы найти код валюты в кэше. Если для запрошенного кода валюты есть действительная запись в кэше, вы возвращаете закэшированный обменный курс, и остальная часть метода пропускается. Я вернусь к оценке того, истекла ли запись чуть позже. Т олько если действующего закэшированного обменного курса нет, вы вызываете внутренний Currency , чтобы получить обменный курс. Прежде чем вернуть его, необходимо его кэшировать. Первый шаг состоит в вычислении срока истечения, и тут вы используете TimeProvider Am bient Cont ext , вместо более традиционного DateTime.Now . С вычисленным сроком истечения вы можете кэшировать запись перед возвратом результата. Вычисление того, истек ли срок действия кэша, также делается при помощи TimeProvider Ambient Context . return TimeProvider.Current.UtcNow >= this.expiration; 156 Класс CachingCurrency использует TimeProvider Am bient Context во всех местах, где ему нужно текущее время, так что можно написать модульный тест, который точно контролирует время. Модификация времени При модульном тестировании класса CachingCurrency , вы можете точно контролировать время совершенно независимо от часов реальной системы. Это позволяет писать детерминистические модульные тесты, хотя тестируемая система (System Under T est, SUT) зависит от концепции текущего времени. Следующий листинг показывает тест, который проверяет, что хотя SUT запрашивает обменный курс четыре раза, внутренняя валюта вызывается только дважды: при первом запросе и снова, когда истекает время действия кэша. Листинг 4-15: Юнит тест на предмет того, что валюта корректно кэшируется и что срок действия корректно заканчивается 1. [Fact] 2. public void InnerCurrencyIsInvokedAgainWhenCacheExpires() 3. { 4. // Fixture setup 5. var currencyCode = "CHF"; 6. var cacheTimeout = TimeSpan.FromHours(1); 7. var startTime = new DateTime(2009, 8, 29); 8. var timeProviderStub = new Mock 9. timeProviderStub 10. .SetupGet(tp => tp.UtcNow) 11. .Returns(startTime); 12. TimeProvider.Current = timeProviderStub.Object; 13. var innerCurrencyMock = new Mock 14. innerCurrencyMock 15. .Setup(c => c.GetExchangeRateFor(currencyCode)) 16. .Returns(4.911m) 17. .Verifiable(); 18. var sut = 19. new CachingCurrency(innerCurrencyMock.Object, 20. cacheTimeout); 21. sut.GetExchangeRateFor(currencyCode); 22. sut.GetExchangeRateFor(currencyCode); 23. sut.GetExchangeRateFor(currencyCode); 24. timeProviderStub 25. .SetupGet(tp => tp.UtcNow) 26. .Returns(startTime + cacheTimeout); 27. // Exercise system 28. sut.GetExchangeRateFor(currencyCode); 29. // Verify outcome 30. innerCurrencyMock.Verify( 31. c => c.GetExchangeRateFor(currencyCode), 32. Times.Exactly(2)); 33. // Teardown (implicit) 34. } Строка 12: Установка TimeProvider Am bient Context Строка 21: Должна быть вызвана внутренняя валюта Строки 22-23: Должна быть закэширована 157 Строки 24-26: Время истечения срока действия Строка 28: Должна быть вызвана внутренняя валюта Строки 30-32: Проверка на то, что внутренняя валюта была вызвана правильно Внимание, жаргон Следующий текст содержит некоторую терминологию модульного тестирования: я выделил ее курсивом, а поскольку эта книга не о модульном тестировании, я отправляю вас к книге |