Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
создание своих зависимостей, но что насчет уничтожения? Как правило, мы не контролируем то, когда в .NET уничтожаются объекты. Сборщик мусора (garbage collector) собирает неиспользуемые объекты, но до тех пор, пока мы не будем работать с устраняемыми объектами, мы не сможем явно уничтожить объект. Примечание Я использую термин уст раняемый объект (disposable object) в качестве условного обозначения экземпляро в объектов типов, которые реализуют интерфейс IDisposable Объекты уничтожаются сборщиком мусора, когда они выходят за рамки области применения. Наоборот, они действуют, пока кто-нибудь еще ссылается на них. Несмотря на то, что потребитель не может явно уничтожить объект, он может сохранять объект, продолжая ссылаться на него. Именно это мы и делаем при использовании Constructor Injection, поскольку мы сохраняем зависимость в приватном поле. Но как демонстрирует рисунок 8-3, когда потребитель выходит за рамки области применения, то же самое может сделать и зависимость. Рисунок 8-3: Кто бы ни внедрял зависимость в потребителя, он решает, когда создается эта зависимость, но потребитель может сохранять зависимость, продолжая ссылаться на нее. Когда потребитель выходит за границы области применения, зависимость может удовлетворять условиям, необходимым для работы сборщика мусора. Даже когда потребитель выходит за границы области применения, зависимость может существовать, если другие объекты содержат ссылку на нее. В противном случае она будет уничтожена сборщиком мусора. Поскольку вы являетесь опытным .NET разработчиком, это довольно не ново для вас, но сейчас обсуждение станет для вас более интересным. 285 Усложнение жизненного цикла зависимости До настоящего момента наш анализ жизненного цикла зависимостей был обыденным, но мы можем добавить в него некоторую сложность. Что происходит, когда для более чем одного потребителя необходима одна и та же зависимость? Один из вариантов – дополнить каждого потребителя своим собственным экземпляром, как это продемонстрировано в следующем листинге. Листинг 8-1: Компоновка с помощью различных экземпляров одной и той же зависимости 1. var repositoryForPolicy = 2. new SqlDiscountRepository(connectionString); 3. var repositoryForCampaign = 4. new SqlDiscountRepository(connectionString); 5. var discountPolicy = 6. new RepositoryBasketDiscountPolicy( 7. repositoryForPolicy); 8. var campaign = 9. new DiscountCampaign(repositoryForCampaign); Строка 6-7, 9: Внедряет соответствующий репозиторий В данном примере для двоих потребителей необходим экземпляр DiscountRepository , поэтому вы присоединяете два отдельных экземпляра с одинаковой строкой соединения. Т еперь вы способны передать repositoryForPolicy в новый экземпляр RepositoryBasketDiscountPolicy , а repositoryForCampaign в новый экземпляр DiscountCampaign Когда дело доходит до жизненных циклов каждого репозитория из листинга 8-1, то по сравнению с предыдущим примером ничего не изменилось. Каждый из них выходит за рамки области применения и уничтожается сборщиком мусора, когда потребитель выходит за рамки области применения. Это может происходить в разное время, но ситуация лишь в малой степени отличается от той, что была до этого. Несколько другой была бы ситуация, если бы оба потребителя были обязаны совместно использовать одну и ту же зависимость, как это продемонстрировано в примере: var repository = new SqlDiscountRepository(connectionString); var discountPolicy = new RepositoryBasketDiscountPolicy(repository); var campaign = new DiscountCampaign(repository); Вместо создания двух различных экземпляров SqlDiscountRepository , вы создаете единичный экземпляр, который внедряете в оба потребителя. Оба потребителя сохраняют ссылку для последующего использования. П римечание Потребители совершенно не осведомлены о том, что зависимость используется ими совместно. Поскольку они оба принимают любую передаваемую им зависимость, для размещения этого изменения в конфигурации зависимости не нужно никакой модификации исходного кода. Это результат принципа подстановки Барбары Лисков. 286 П ринцип подстановки Барбары Лисков Как утверждалос ь ранее, принцип подстановки Барбары Лисков является теоретической и абстрактной сущностью. Но в объектно-ориентированном программировании мы можем перефразировать этот принцип следующим образом: Мет оды, использующ ие абст ракции, должны уметь использовать любой унаследованный класс, ничего при этом не зная о нем. Другими словами, мы должны уметь заменять абстракцию произвольной реализацией, не изменяя при этом точность системы. Ситуация, связанная с жизненным циклом зависимости repository , отчетливо изменилась по сравнению с предыдущим примером. Оба потребителя должны выйти за рамки области применения до того, как repository сможет приобрести право быть уничтоженным сборщиком мусора, и сделать они это могут в разное время. Ситуация становится менее предсказуемо й, когда зависимость достигает момента завершения своего жизненного цикла, и эта особенность только усиливается при увеличении числа потребителей. При достаточном количестве потребителей, скорее всего, поблизости всегда будет находиться один из них, который сохраняет зависимость "живой". Это может казаться проблемой, но такое редко случается: вместо множества схожих экземпляров, мы имеем только один, который сохраняет память. Это настолько завидное качество, что мы формализуем его в паттерне стиля существования Singleton. Несмотря на их схожесть, не путайте его с паттерном проектирования Singleton. Более подробно этот вопрос мы рассмотрим в разделе "Singleton". Ключевым моментом, который стоит принять во внимание, является тот факт, что Composer обладает большей степенью влияния на жизненный цикл зависимостей, нежели любой единичный потребитель. Composer решает, когда создаются зависимости, и на основании своего выбора, использовать ли экземпляры совместно или нет, определяет, выходит ли зависимость за рамки области применения с единственным потребителем, или все потребители должны выйти за рамки области применения до того, как могут быть освобождены зависимости. Это сравнимо с посещением ресторана в компании хорошего сомелье. Сомелье проводит значительную часть дня, управляя винным подвалом и совершенствуя его содержимое, покупая новые вина, пробуя вино из доступных бутылок для отслеживания того, как развивается это вино, а также, работая с шеф-поварами, чтобы определить оптимальное соответствие подаваемым блюдам. При ознакомлении с винной картой мы видим только то, что сомелье посчитал нужным представить к продаже. Мы вольны выбрать вино в соответствии с нашим личным вкусом, но мы не допускаем, что знаем больше сомелье о выборке вин ресторана, и о том, как эти вина сочетаются с блюдами. Сомелье будет часто принимать решение о хранении большого количества бутылок в хранилище на протяжении многих лет; и как вы увидите в следующем разделе, Composer может принять решение о хранении экземпляро в "живыми", продолжая поддерживать ссылки на эти экземпляры. У правление жизненным циклом с помощью контейнера В предыдущем разделе объяснялось, как мы можем варьировать композицию зависимостей для того, чтобы влиять на их жизненный цикл. В данном разделе мы будем рассматривать то, как DI-контейнер может обращаться к этим вариациям. 287 П римечание В данном разделе обсуждаются принципы, лежащие в основе управления жизненными циклами с помощью DI-контейнера, поэтому я не буду подробно рассматривать конкретные контейнеры. Как и на протяжении всей части 3, я использую Poor Man's DI для иллюстрации этих сущностей. Мы начнем с рассмотрения того, как контролировать жизненный цикл зависимостей с помощью пользовательских контейнеров, а затем перейдем к легко реализуемому примеру задания стилей существования в реальном DI-контейнере. Управление стилями существования с помощью специализирова нного контейнера В главе 7 мы создали специализированные контейнеры для построения приложений. Одним из таких контейнеров был CommerceServiceContainer . Листинг 7-9 демонстрирует реализацию его метода ResolveProductManagementService ; и, как показывает рисунок 8- 4, этот метод является единственным кодом в данном классе. Рисунок 8-4: Вся реализация класса CommerceServiceContainer в настоящий момент располагается в методе ResolveProductManagementService . Метод Release абсолютно ничего не делает, кроме того, в классе нет ни полей, ни свойств. Если вам интересно, почему здесь присутствует метод Release , то мы вернемся к этому вопросу в разделе "Управление устраняемыми зависимостями". Как вы можете помнить из листинга 7-9, метод Resolve создает полноценную диаграмму зависимостей, каждый раз, когда он вызывается. Другими словами, каждая зависимость является приватной по отношению к рассматриваемому IProductManagementService , и какие-либо связи отсутствуют. Когда экземпляр IProductManagementService выходит за рамки области применения (что происходит всякий раз, когда сервис отвечает на запрос), все зависимости также выходят за рамки области применения. Это часто называют стилем существования Transient (кратковременным), но подробнее о нем мы поговорим в разделе "T ransient ". Давайте проанализируем диаграмму объектов, созданную CommerceServiceContainer и продемонстрированную рисунком 8-5, на факт существования возможности для совершенствования. 288 Рисунок 8-5: Диаграмма объектов, созданная CommerceServiceContainer . Каждый созданный экземпляр ProductManagementService содержит свой собственный ContractMapper и свой собственный SqlProductRepository , который, в свою очередь, содержит собственную строку соединения. Зависимости, показанные справа, являются неизменными. Класс ContractMapper является совершенно не сохраняющим свое состояние сервисом, поэтому нет необходимости создавать новый экземпляр всякий раз, когда нам нужно обслужить запрос. Строка соединения также, скорее всего, не изменяется, поэтому мы можем также решить повторно использовать ее в рамках запросов. Класс SqlProductRepository , с другой стороны, полагается на Ent ity Framework Object Context , и считается хорошим тоном использовать для каждого запроса новый экземпляр. При данной конкретной конфигурации наилучшая реализация CommerceServiceContainer снова использовала бы те же самые экземпляр ы как ContractMapper , так и строки соединения, при создании новых экземпляров SqlProductRepository . Короче говоря, вам следует сконфигурировать ContractMapper и строку соединения таким образом, чтобы они использовали стиль существования Singleton и SqlProductRepository в виде Transient. Следующий листинг демонстрирует, как реализовать данное изменение. Листинг 8-2: Управление жизненным циклом с помощью контейнера 1. public partial class LifetimeManagingCommerceServiceContainer : 2. ICommerceServiceContainer 3. { 4. private readonly string connectionString; 5. private readonly IContractMapper mapper; 6. public LifetimeManagingCommerceServiceContainer() 7. { 8. this.connectionString = 9. ConfigurationManager.ConnectionStrings 10. ["CommerceObjectContext"].ConnectionString; 11. this.mapper = new ContractMapper(); 12. } 13. public IProductManagementService 14. ResolveProductManagementService() 15. { 16. ProductRepository repository = 17. new SqlProductRepository( 18. this.connectionString); 19. Return new ProductManagementService( 20. repository, this.mapper); 21. } 22. } Строка 8-11: Создает Singleton зависимости Строка 16-18: Создает T ransient зависимость 289 Поскольку вы хотите повторно использовать строку соединения и ContractMapper в рамках всех запросов, вы сохраняете их в приватных полях и инициализируе те в конструкторе. Ключевое слово readonly обеспечивает дополнительную гарантию того, что, будучи единожды заданными, эти Singleton экземпляр ы остаются неизменными и не могут быть заменены, но кроме этой дополнительной гарантии, readonly никоим образом не требуется при реализации стиля существования Singleton. Каждый раз, когда контейнер просят создать новый экземпляр, он создает T ransient экземпляр SqlProductRepository с помощью Singleton строки соединения. В конечном счете, контейнер использует этот Transient repository вместе с Singleton mapper для того, чтобы скомпоновать и вернуть экземпляр ProductManagementService П римечание Код в листинге 8-2 функционально эквивалентен коду из листинга 7-9, но только слегка более эффективен. Продолжая ссылаться на создаваемые им зависимости, контейнер может сохранять их в жизнеспособном состоянии столь долго, сколько он того хочет. В предыдущем примере он создает обе зависимости, как только они инициализиру ются, но он мог использовать и Lazy инициализацию. Данный пример должен дать вам представление о том, как DI-контейнеры управляют жизненными циклами. Поскольку DI-контейнер является повторно используемой библиотекой, мы не можем изменять его исходный код каждый раз, когда нам хочется переконфигурировать стиль существования. В следующем разделе мы вкратце рассмотрим то, как конфигурировать стили существования для шаблонного контейнера. Управление стилем существования с помощь ю Autofac Время от времени на протяжении этой книги я делаю передышку от Poor Man's DI, чтобы предоставить пример того, как мы можем достичь результата с помощью шаблонного DI- контейнера. Каждый DI-контейнер имеет свой собственный конкретный API для выражения множества различных признаков; но, несмотря на то, что детали различаются, принципы остаются теми же. Это справедливо и для механизма управления жизненным циклом. П римечание Даже термин "управление жизненным циклом" не является вездесущим. Например, Autofac называет этот процесс Област ью применения экземпляра (Instance Scope). В данном разделе мы вкратце будем рассматривать конфигурирование жизненных циклов с помощью Autofac. П римечание Нет какой-то конкретной причины того, почему для данного примера я предпочел Autofac другим DI-контейнерам. Т аким же образом я мог выбрать и любой другой DI-контейнер. Следующий листинг демонстрирует, как сконфигурировать Autofac с помощью простых T ransient зависимостей аналогично примеру из листинга 7-9. 290 Листинг 8-3: Конфигурирование Autofac с помощью Transient зависимостей var builder = new ContainerBuilder(); builder.RegisterType .As ConfigurationManager .ConnectionStrings["CommerceObjectContext"] .ConnectionString)) .As (); builder.RegisterType () .As Одной из особенностей Autofac является то, что вы не конфигурируете сам контейнер, а конфигурируете ContainerBuilder и используете его для создания контейнера при завершении конфигурации. Самая простая форма регистрации – это когда вам нужно определить только преобразование между абстракцией и конкретным типом, например, преобразование IContractMapper в ContractMapper . Обратите внимание на то, что конкретный тип указан перед абстракцией, что является порядком, противоположным тому, который используется большинством DI-контейнеров. Несмотря на то, что Autofac так же, как и другие DI-контейнеры поддерживает автоматическую интеграцию, внедрение таких примитивных типов, как строки, всегда представляет собой особый случай, поскольку здесь может потенциально использоваться множество различных строк. В данном случае вы имеете всего одну строку соединения, но вам нужно еще передать ее в SqlProductRepository , который вы на данный момент регистрируете. Вы можете сделать это с помощью лямбда-выражения, которое будет выполняться при запросе типа ProductRepository Использование лямбда-выраже ний – одна из заявок Autofac на успех. Несмотря на то, что большинство DI-контейнеров на данный момент обладают похожим свойством, Autofac был одним из первых контейнеров, познакомивших нас с лямбда-выражения ми. Вы можете использовать лямбда, чтобы указать, как создается класс SqlProductRepository , и что еще более специфично, вы вытягиваете параметр конструктора connectionString из конфигурации приложения. Преимущество использования лямбда-выражений заключается в том, что они безопасны относительно типов, поэтому вы получаете статическую верификацию создания SqlProductRepository . Недостаток – вы не получаете автоматическу ю интеграцию, поэтому до тех пор, пока вам не нужно явно указывать параметр конструктора, предпочтительнее всего является более простое преобразование с помощью RegisterType . Это и есть то, как вы преобразуете IProductManagementService в ProductManagementService , поэтому он может воспользоват ься преимущество м автоматической интеграции. Т еперь вы можете использовать экземпляр container для создания новых экземпляров IProductManagementService , подобных следующему: var service = container.Resolve 291 Но постойте, а что насчет управления жизненным циклом? Большинство DI-контейнеров обладают стилем существования по умолчанию. В случае Autofac используемый по умолчанию стиль называется Per Dependency, что то же самое, что и стиль существования T ransient . Поскольку он является используемым по умолчанию, вам не нужно было указывать его, но если вы захотите, то можете сделать это следующим образом: builder.RegisterType .As .InstancePerDependency(); Обратите внимание на то, что вы используете свободный интерфейс регистрации для того, чтобы определить област ь применения экземпляра (термин Autofac используемый вместо термина "стиль существования") при помощи метода InstancePerDependency Т акже существует Single Instance Scope (единичная область применения), которая соответствует стилю существования Singleton. Вооружившись этими знаниями, вы можете создать Autofac – аналог листинга 8-2: builder.RegisterType .As |