Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
В этом примере кода контейнер конфигурируется таким образом, что он с одноминутной задержкой использует CacheLifetimeManager для экземпляра IIngredient . В рамках одноминутного промежутка вы можете запрашивать какое угодно количество диаграмм объектов, и всякий раз, когда диаграмма содержит экземпляр IIngredient , вы будете получать один и тот же SauceBéarnaise . По истечении одноминутной задержки при последующих запросах будет возвращаться новый экземпляр SauceBéarnaise 555 Разрешение компонентов с пользовательским стилем существования выполняется обычным образом. Сюрпризы начинаются только, когда мы пытаемся высвободить разрешенные диаграммы объектов. Высвобожде ние компонентов с пользовательским стилем существования Как я уже упоминал, метод RemoveValue никогда не вызывается контейнером Unity. Т ем не менее, если нам необходимо добавить функциональность постобработки для пользовательского LifetimeManager , мы можем заставить LifetimeManager реализовать IDisposable . Это приведет к тому, что пользовательский LifetimeManager будет уничтожен при уничтожении контейнера, являющегося владельцем этого LifetimeManager Однако могут возникать и другие сюрпризы, но не будем заострять на них внимание. В листинге 14-3 вы уже видели, что CacheLifetimeManager реализует IDisposable , но в следующем листинге вы впервые увидите саму реализацию. Листинг 14-4: Уничтожение LifetimeManager 1. public void Dispose() 2. { 3. GC.SuppressFinalize(this); 4. this.Dispose(true); 5. } 6. protected virtual void Dispose(bool disposing) 7. { 8. if (disposing) 9. { 10. var d = this.value as IDisposable; 11. if (d != null) 12. { 13. d.Dispose(); 14. } 15. this.value = null; 16. } 17. } Строка 10-15: Уничтожение устраняемого объекта Класс CacheLifetimeManager реализует IDisposable , руководствуясь при этом стандартным паттерном Dispose. Если полученное значение реализует IDisposable , вы его уничтожаете. Но в любом случае вы устанавливаете в поле value значение null , чтобы сборщик мусора мог уничтожить этот компонент. Согласно теории, если вы регистрируете и разрешаете устраняемый компонент с помощью CacheLifetimeManager , компонент должен быть уничтожен вместе с контейнером следующим образом: var lease = new SlidingLease(TimeSpan.FromMinutes(1)); var cache = new CacheLifetimeManager(lease); container.RegisterType Согласно документации, которая прилагается к контейнеру Unity, при устранении контейнера также выполняется высвобождение ingredient . Как известно, петрушку 556 нельзя разогревать, поэтому класс Parsley , очевидно, является устраняемым. При уничтожении контейнера уничтожается и экземпляр Parsley . Пока все идет так, как надо. Т ем не менее, при создании и уничтожении дочернего контейнера вы будете рассчитывать на то, что устраняемый LifetimeManager будет работать так же, как и HierarchicalLifetimeManager : 1. IIngredient ingredient; 2. using (var child = container.CreateChildContainer()) 3. { 4. ingredient = child.Resolve 5. } Строка 4: Ingredient не уничтожается При наличии конфигурации компонента Parsley , аналогичной той, что продемонстрирована в предыдущем примере, вы рассчитываете на то, что ingredient будет устранен при уничтожении дочернего контейнера. Увы, этого не происходит. CacheLifetimeManager.Dispose никогда не вызывается. П редупреждение Даже когда LifetimeManager реализует IDisposable , метод Dispose вызывается только при уничтожении контейнера-владельца. Как такое может происходить, если в аналогичном коде, который вы уже видели в разделе 14.2.1 "Конфигурирование стиля существования", использовался HierarchicalLifetimeManager ? Оказывается, контейнер Unit y обладает соответствующим BuilderStrategy , который содержит особую логику HierarchicalLifetimeManager , позволяющу ю использовать эту функциональность. Хорошая новость – мы можем сделать то же самое. Ре ализация пользовательской Life tim eStrategy Причиной того, что HierarchicalLifetimeManager работает должным образом, является тот факт, что Unity обладает BuilderStrategy , который создает копию HierarchicalLifetimeManager , содержащейся в родительском контейнере, и связывает ее с дочерним контейнером. Это позволяет дочернему контейнеру уничтожать LifetimeManager , когда он сам устраняется. Т о же самое мы можем сделать, реализовав пользовательский BuilderStrategy , продемонстрированный в следующем листинге. Листинг 14-5: Реализация пользовательского LifetimeStrategy 1. public class CacheLifetimeStrategy : BuilderStrategy 2. { 3. public override void PreBuildUp( 4. IBuilderContext context) 5. { 6. if (context == null) 7. { 8. throw new ArgumentNullException("context"); 9. } 10. IPolicyList policySource; 11. var lifetimePolicy = context 12. .PersistentPolicies 557 13. .Get 14. out policySource); 15. if (object.ReferenceEquals(policySource, 16. context.PersistentPolicies)) 17. { 18. return; 19. } 20. var cacheLifetime = 21. lifetimePolicy as CacheLifetimeManager; 22. if (cacheLifetime == null) 23. { 24. return; 25. } 26. var childLifetime = cacheLifetime.Clone(); 27. context 28. .PersistentPolicies 29. .Set 30. context.BuildKey); 31. context.Lifetime.Add(childLifetime); 32. } 33. } Строка 3-4: Переопределение PreBuildUp Строка 10-14: Получение стратегии жизненного цикла Строка 15-19: Проверка принадлежно сти Строка 20-25: Проверка типа Строка 26: Создание копии Строка 27-31: Смена стратегии жизненного цикла CacheLifetimeStrategy наследуется от абстрактного класса BuilderStrategy и реализует метод PreBuildUp , который вызывается всякий раз, когда контейнер Unity создает новый экземпляр. Это дает вам возможность изменить контекст до создания объекта. Первое, что вам необходимо сделать, – получить текущий ILifetimePolicy для компонента. Контекст может предоставить эту информацию, а также информацию об источнике стратегии. Экземпляр policySource косвенным образом сообщает вам о том, где определен стиль существования. Если источником является родительский контейнер, но в настоящий момент вы выполняете настройку в рамках дочернего контейнера, то предполагается, что источник стиля существования будет отличаться от текущего контекста, если стиль существования первоначально был определен в родительском контейнере. Это то, что нам надо, поэтому вы преждевременно выходите из метода. Эта конкретная реализация затрагивает только CacheLifetimeManager , поэтому если в качестве стиля существования используется какой-то другой стиль, вы также выходите из метода. С другой стороны, если в качестве стиля существования используется стиль CacheLifetimeManager , то вы создаете его копию, которая будет использоваться в дочернем контейнере. 558 Если вы точно уверены в том, что на данный момент выполняете настройку в рамках дочернего контейнера, то добавляете копию CacheLifetimeManager обратно в контекст, фактически, заменяя унаследованный из родительского контейнера стиль существования на стиль, необходимый для этого конкретного дочернего контейнера. Это достаточно сложно, и потом, вы еще даже не все до конца выполнили. Несмотря на то, что вы реализовали пользовательский BuilderStrategy , вы еще не сообщили об этом Unity. К счастью, как демонстрирует следующий листинг, это намного проще, чем реализация CacheLifetimeStrategy Листинг 14-6: Расширение Unity с помощью CacheLifetimeStrategy public class CacheLifetimeStrategyExtension : UnityContainerExtension { protected override void Initialize() { this.Context.Strategies .AddNew UnityBuildStage.Lifetime); } } Чтобы добавить CacheLifetimeStrategy в контейнер Unit y, вы создаете новое расширение контейнера. Помните, как вы использовали расширения контейнера для пакетирования конфигурации в разделе 14.1.3 "Пакетирование конфигурации"? Здесь же представлен еще один пример, возможно, более идиоматического применения расширения контейнера. В методе Initialize вы добавляете контекст CacheLifetimeStrategy , наряду с информацией о том, что этот конкретный BuilderStrategy предназначен для управления жизненным циклом. Наконец, выполнив все это, вы можете расширить контейнер Unit y таким образом, что CacheLifetimeManager теперь функционирует точно так же, как и HierarchicalLifetimeManager : container.AddNewExtension После добавления этого расширения контейнера сценарий, который ранее не работал, в конце концов, становится работоспособным: для высвобождения объектов со стилем CacheLifetimeManager вы можете воспользоваться дочерними контейнерами. Теперь, когда вы изучили BuilderStrategy , мы можем закончить цикл обучения и реализовать поддержку метода Teardown Ре ализация поддержки метода Teardown Когда мы в разделе 14.2.1 "Конфигурирование стиля существования" приступали к обсуждению процесса высвобождения компонентов, мы сразу же опустили метод Teardown , поскольку он не высвобождал компоненты должным образом. С другой стороны, это не должно было привести вас к мысли о том, что метод Teardown бесполезен. Напротив, он вызывает различные методы зарегистрированных BuilderStrategy . Это означает, что мы можем реализовать пользовательский BuilderStrategy , который должным образом высвобождает компоненты в рамках Teardown 559 Было бы хорошо, если бы Teardown поддерживал CacheLifetimeManager . К счастью, несмотря на то, что поддержка CacheLifetimeManager подразумевает создание еще одного BuilderStrategy (или расширение уже созданного нами BuilderStrategy ), листинг 14-7 демонстрирует, что это намного проще, чем реализация CacheLifetimeStrategy , приведенного в листинге 14-5. Листинг 14-7: Реализация стратегии высвобождения 1. public class CacheReleasingLifetimeStrategy : BuilderStrategy 2. { 3. public override void PostTearDown( 4. IBuilderContext context) 5. { 6. if (context == null) 7. { 8. throw new ArgumentNullException("context"); 9. } 10. var lifetimes = context 11. .Lifetime.OfType 12. foreach (var lifetimePolicy in lifetimes) 13. { 14. lifetimePolicy.RemoveValue(); 15. } 16. } 17. } Строка 3-4: Реализация PostTearDown Строка 6-9: Граничный оператор Строка 10-15: Высвобождение значения Вместо того, чтобы переопределять метод PreBuildUp , как вы поступали в листинге 14-5, вы переопределяете метод PostTearDown , который вызывается из метода TearDown после того, как списывается большинство остальных ресурсов рассматриваемого компонента. Контекст имеет целый список объектов, определяющих стили существования, но, предполагается, что в большинстве случаев контекст содержит только один единственный экземпляр CacheLifetimeManager . Все-таки, для чистоты эксперимента, вы делаете вид, будто в контексте может присутствовать любое количество таких объектов, и вызываете метод RemoveValue для каждого из них. Как вы можете помнить из реализации CacheLifetimeManager , приведенной в листинге 14-3, метод RemoveValue удаляет и устраняет соответствующее значение только по истечении срока аренды. И вот вы уже близки к завершению цикла. Можете добавить CacheReleasingLifetimeStrategy к CacheLifetimeStrategyExtension , приведенному в листинге 14-6: this.Context.Strategies .AddNew UnityBuildStage.Lifetime); this.Context.Strategies .AddNew UnityBuildStage.Lifetime); 560 В конце концов, это позволяет вам высвободить кэшированные компоненты с помощью метода Teardown : container.AddNewExtension После создания такой инфраструктуры переменная ingredient , которая на самом деле является экземпляром устраняемого класса Parsley , должным образом высвобождает ся при вызове метода Teardown . По истечении срока аренды экземпляр уничтожается, тем не менее, если срок аренды не истек, то ничего не происходит. После того как мы добавили все эти LifetimeManager , BuilderStrategy и расширения контейнера Unit y наконец-то начинает функционировать так, как нам и нужно, то есть, та его часть, которая относится к стилю существования cache. Вспомните, что стили T ransient и Per Graph все равно не ведут себя так, как нам бы того хотелось. П одсказка Несмотря на то, что ни TransientLifetimeManager , ни PerResolveLifetimeManager не реализуют IDisposable и в их методах RemoveValue не исполняется никакой логики, они, по крайней мере, не изолированы. Если мы хотим, чтобы они должным образом высвобождали компоненты, то можем выполнить наследование от этих классов, а затем не забыть реализовать соответствующие BuilderStrategy В заключении, мы должны признать, что хотя Unity и обладает множеством Seam 'ов, необходимых для желаемой нами реализации пользовательских стилей существования, сделать это очень сложно. С другой стороны, это хотя бы возможно, чего мы не можем сказать о некоторых других DI-контейнерах. Можно было бы упаковать такой пользовательский стиль существования в повторно используемую библиотеку затем, чтобы нам, по крайней мере, не пришлось повторно реализовывать этот стиль существования для каждого нового приложения, которое мы планируем создать. Это тоже нужно, поскольку модель управления жизненным циклом, используемая контейнером Unity, не позволяет реализовать все наши потребности, несмотря на очевидную исчерпывающую поддержку им различных стилей существования. На этом наш обзор механизма управления жизненным циклом контейнера Unit y подошел к концу. По сравнению с остальными разделами, посвященными конкретным DI- контейнерам, этот раздел довольно объемный. Частично это зависит от множества различных возможностей, доступных нам при реализации пользовательских стилей существования. С другой стороны, такой объем объясняется некоторыми уникальными ловушками, связанными с механизмом управления жизненным циклом, которые мне хотелось бы рассмотреть. Конфигурировать компоненты можно и посредством сочетания различных стилей существования. Это справедливо и при регистрации составных реализаций одной и той же абстракции. Мы уже рассматривали процесс работы с составными компонентами, но в следующем разделе этот вопрос обсуждается более углубленно. Unit y позволяет нам более подробно изучить этот процесс, поскольку он 561 поддерживает механизм перехвата. Следующий раздел можно рассматривать как расширение обсуждения Decorator'ов. 562 14.3. Работа с составными компонентами DI-контейнеры процветают благодаря их индивидуа льности, но их неопределенность порождает ряд трудностей. При использовании Constructor Injection единичный конструктор предпочтительнее перегружаемых конструкторов, поскольку в этом случае ясно, какой конструктор использовать в ситуации, когда у вас нет выбора. То же самое касается и преобразования абстракций к конкретным типам. Если мы пытаемся преобразовать конкретные составные типы к одной и той же абстракции, это приводит к неопределенности. Несмотря на столь нежелательну ю особенность как неопределенность, нам часто приходится работать с составными реализациями единичного интерфейса. Это может происходить в следующих ситуациях: Для разных потребителей должны использоваться разные специфичные типы Зависимости являются последовательнос тями Используются Decorator'ы В этом разделе мы рассмотрим каждую из этих ситуаций и увидим, как Unit y поочереди справляется с каждой из них. После прочтения раздела вы должны будете уметь регистрировать и разрешать компоненты даже тогда, когда в дело вступают составные реализации одной и той же абстракции. Рассмотрим сначала способы предоставления более тщательного контроля, нежели тот, который предоставляет механизм автоматической интеграции. В ыбор среди составных кандидатов Автоматическая интеграция – удобный и мощный инструмент, но предоставляет нам меньшие возможности контроля. Пока все абстракции преобразуются в конкретные типы отдельно друг от друга, никаких трудностей не возникает, но как только мы вводим большее количество реализаций для одного и того же интерфейса, возникает неопределенность. Способ работы контейнера Unity с составными регистрациями одной и той же абстракции слегка отличается от того, который используется другими DI-контейнерами. С рассмотрения этого вопроса мы и начнем изучение раздела, поскольку с помощью него устанавливаются некоторые фундаментальные правила, которые понадобятся нам в остальной части раздела. Ре гистрация составны х реализаций одного и того же компонента Как вы уже видели в разделе 14.1.2 "Конфигурирование контейнера", вы можете регистрировать составные компоненты одного и того же сервиса: container.RegisterType В этом коде может присутствовать только одна неименованная регистрация. Она называется регист рацией по умолчанию. Если вы впоследствии вызовете для IIngredient 563 метод RegisterType без имени, то регистрация Steak будет заменена новым компонентом. П римечание Регистрация по умолчанию для типа может быть только одна, а именованных регистраций может быть сколько угодно. При вызове метода Resolve без имени мы получаем объект на основании регистрации по умолчанию. Благодаря предыдущей конфигурации метод Resolve возвращает экземпляр Steak : var ingredient = container.Resolve Об именованной регистрации sauce мы не забыли. Разрешать составные IIngredient можно следующим образом: IEnumerable В соответствии с конфигурацией, приведенной в предыдущем примере, вы получаете последовательност ь, содержащую не экземпляр Steak , а экземпляр SauceBéarnaise П редупреждение Метод ResolveAll возвращает все именованные регистрации, но не регистрацию по умолчанию. Если существуют сконфигурированные экземпляры плагина, которые не могут быть разрешены при вызове метода ResolveAll , Unit y выдает исключение, поясняющее, что существуют зависимости, неудовлетворя ющие условиям. Такое поведение совместимо с поведением метода Resolve , но отлично от того, как поступают Castle Windsor или MEF. В следующем листинге демонстрируется, как можно использовать именованные регистрации для предоставления подсказок, которые позднее можно применять при осуществлении выбора между различными сконфигурированными компонентами. Листинг 14-8: Присваивание имен регистрациям container.RegisterType Каждой регистрации можно присвоить свое уникальное имя, которое в дальнейшем можно применять для различения похожих компонентов. П римечание Существует возможность регистрировать для типа только именованные компоненты. Если мы так сделаем, то у типа не будет регистрации по умолчанию. Благодаря именованным регистрациям, приведенным в листинге 14-8, вы можете разрешить и Steak , и SauceBéarnaise следующим образом: 564 var meat = container.Resolve Обратите внимание на то, что вы передаете тот же самый идентификатор, который использовали для именования компонента во время регистрации. Принимая во внимание тот факт, что мы всегда должны разрешать сервисы в единственном Com position Root, мы, скорее всего, не должны ждать появления такой неопределенности на этом уровне. П одсказка Если вы обнаружите, что вызываете метод Resolve с конкретным именем или идентификатором, подумайте над тем, сможете ли вы сменить свой подход на менее неопределенный. Именованные регистрации можно использовать для осуществления выбора среди нескольких экземпляров при конфигурировании зависимостей данного сервиса. Конфигурирова ние именованны х зависимостей Иногда бывает необходимо переопределить обычное поведение для того, чтобы обеспечить более разветвленный контроль над тем, куда какая зависимость отправляется. Кроме того, возможны ситуации, при которых приходится сталкиваться с неопределенным API. В качестве примера рассмотрим следующий конструктор: public ThreeCourseMeal(ICourse entrée, ICourse mainCourse, ICourse dessert) В этом примере присутствуют три одинаковым образом типизированных зависимости, каждая из которых является отдельной, не похожей на другие сущностью. В большинстве случаев необходимо преобразовывать каждую из этих зависимостей в отдельный тип. В следующем листинге продемонстрированы способы конфигурации преобразований ICourse Листинг 14-9: Регистрация именованных course container.RegisterType Как вы уже делали это в листинге 14-8, вы регистрируете три именованных компонента, преобразуя Rilettes в экземпляр под названием "entrée", CordonBleu – в экземпляр с именем "mainCourse", а MousseAuChocolat – в экземпляр под названием "dessert". Принимая во внимание эти регистрации, теперь вы можете зарегистрировать класс ThreeCourseMeal так, как это продемонстрировано в следующем листинге. Листинг 14-10: Переопределение механизма автоматической интеграции container.RegisterType 565 До этого момента мы еще не обсуждали подробно тот факт, что все перегрузки метода RegisterType принимают в качестве параметра массив объектов класса InjectionMember InjectionMember – это стратегия, которую Unit y использует в качестве инструкции при компоновке типов друг с другом. Например, InjectionConstructor позволяет определять параметры, используемые для паттерна Constructor Injection. Один из способов переопределения механизма автоматической интеграции – задание его с помощью массива экземпляров ResolvedParameter . Каждый ResolvedParameter определяет тип, который необходимо разрешить, а также необязательное имя – это имя именованной регистрации, а не имя аргумента конструктора. ResolvedParameter entrée обозначает регистрацию entrée . Параметры конструктора заполняются позиционным способом, т.е. первый ResolvedParameter соответствует первому аргументу конструктора и т.д. П римечание В отличие от большинства остальных DI-контейнеров, позиционная стратегия позволяет контейнеру Unit y оставаться стабильным при столкновении с такими формами рефакторинга, как изменение имени аргументов конструктора. С другой стороны, если мы переставим местами аргументы конструктора, то все рухнет. Другие DI-контейнеры могут с этим справиться. Переопределение автоматической интеграции посредством явного преобразования параметров в именованные компоненты – общепринятый подход. Делать это можно даже, если именованные компоненты конфигурируются в одном расширении контейнера, а потребитель – в совершенно другом расширении, поскольку единственная идентификация, связывающая именованный компонент с параметром – это имя. Это возможно, но если нам приходится управлять множеством имен, такой подход является довольно хрупким. Когда основной причиной, побуждающей нас использовать именованные компоненты, является неопределенность, наилучшее решение – сконструировать свое собственное API, помогающее избавиться от этой неопределенности. Все это также приводит к лучшей конструкции в целом. В следующем разделе вы увидите, как можно использовать менее неопределенный и более гибкий подход, при котором разрешается использовать в обеде несколько блюд. Т еперь вы должны изучить то, как Unity работает со списками и последовательностями. Интеграция последовательностей В разделе 10.3.2 "Разработка пользовательского стиля существования" мы обсуждали, как выполнить рефакторинг явного класса ThreeCourseMeal к более универсальному классу Meal , который обладает приведенным ниже конструктором: public Meal(IEnumerable В этом разделе мы рассмотрим то, как можно сконфигурировать Unit y, чтобы он интегрировал экземпляры Meal с соответствующими зависимостями ICourse . После рассмотрения этого вопроса вы должны будете приобрести хорошее понимание тех возможностей, которые доступны в случае необходимости конфигурирования экземпляров, имеющих последовательности зависимостей. 566 Автоматическая инте грация последовательностей Unity довольно хорошо разбирается в массивах, но не в других видах последовательност ей, например, IEnumerable или IList . Для эффективной работы с последовательностями мы должны определить или преобразовать их в массивы, чтобы Unity мог работать с ними соответствующим образом. Если вы попытаетесь зарегистрировать Meal , не сообщая при этом контейнеру о том, как ему следует работать с зависимостью IEnumerable , то при попытке разрешить IMeal будет выдаваться исключение: container.RegisterType При разрешении IMeal выдается исключение, потому что Unit y не знает, как нужно разрешать IEnumerable . Т ак произойдет, даже если вы перед этим зарегистрируете несколько компонентов ICourse , как вы и поступали в листинге 14-9. Чтобы преобразовать все именованные регистрации ICourse в IEnumerable , можно воспользоваться преимуществами врожденного понимания контейнером Unity массивов. Самый простой способ сделать это – преобразовать два приведенных ниже типа: container.RegisterType Это может показаться слегка странным, но зато такой подход достаточно хорошо работает. Всякий раз, когда Unity сталкивается с зависимостью IEnumerable , он преобразует ее в запрос массива экземпляров ICourse , что даст нам такой же самый результат, как если бы мы вызвали container.ResolveAll П римечание Unity разрешает массивы подобно тому, как он возвращает результат метода ResolveAll Возвращаются все запрашиваемые именованные компоненты, но не компонент по умолчанию. После выполнения такого преобразования в результате разрешения IMeal возвращается корректный результат: экземпляр Meal с экземплярами ICourse из листинга 14-9: Rillettes , CordonBleu и MousseAuChocolat Unity управляет массивами сообразно тому, как он реализует ResolveAll . Но вот как работать с остальными видами последовательностей, он не знает, поэтому мы должны преобразовать их в массивы, чтобы заставить Unity их понимать. В результате мы получим все зарегистрированные компоненты данного типа, и этого чаще всего бывает достаточно. Только когда нам нужно из большого набора компонентов явно отобрать только несколько из них, нам необходимо выполнить больше действий. Это возможно благодаря более явной конфигурации. О тбор нескольких компонентов из большого набора Когда мы используем способность Unity разрешать массивы, все именованные компоненты внедряются в потребителей. Чаще всего это корректная линия поведения, но, 567 как показывает рисунок 14-7, возможны случаи, когда нам необходимо отобрать только несколько компонентов из большого набора всех зарегистрированных компонентов. Рисунок 14-7: В ситуации, продемонстрированной слева, мы хотим явным образом отобрать определенные зависимости из большого списка всех зарегистрированных компонентов. Это отличается от ситуации, приведенной справа, когда мы отбираем все без разбора. Когда мы ранее позволяли Unity автоматически интегрировать все сконфигурированные экземпляр ы, это соответствовало бы ситуации, изображенной в правой части рисунка 14- 7. Если нам нужно сконфигурировать экземпляр так, как изображено в левой части рисунка, то мы должны явно определить, какие экземпляры необходимо использовать. В листинге 14-10 вы использовали класс InjectionConstructor для того, чтобы определить вместо стратегии автоматической интеграции, используемой по умолчанию, другую стратегию. Когда дело касается класса Meal , вы можете сделать то же самое с единственной лишь разницей, что теперь вместо трех отдельных аргументов ICourse внедряется зависимость IEnumerable . Следующий листинг демонстрирует, как сконфигурировать явный массив с помощью InjectionConstructor Листинг 14-11: Внедрение именованных компонентов в последовательность container.RegisterType Чтобы переопределить механизм автоматической интеграции и явным образом задать стратегию внедрения зависимостей в конструктор Meal , вы еще раз обращаетесь к классу InjectionConstructor . Поскольку для конструктора Meal нужен IEnumerable , 568 вы можете воспользоваться экземпляром ResolvedArrayParameter для определения массива, который будет вычисляться при разрешении контейнером класса Meal . Класс ResolvedArrayParameter определяет стратегию, при которой анализ массива экземпляров ICourse откладывается до тех пор, пока не разрешится сам Meal Чтобы определить значения, которые будут использоваться при разрешении Meal , можно использовать три именованных экземпляра ResolvedParameter , как вы уже делали в листинге 14-10. Единственное отличие – теперь они используются в качестве аргументов для конструктора ResolvedArrayParameter вместо того, чтобы использоваться напрямую в InjectionConstructor . При разрешении IMeal ResolvedArrayParameter разрешает три именованных регистрации entrée, m ainCourse и dessert, а также создает массив ICourse из этих трех компонентов. Поскольку ICourse[] реализует IEnumerable , может подойти конструктор Meal В очередной раз вы видите, что контейнер Unity отлично понимает массивы. Несмотря на то, что он не поддерживает остальные виды последовательнос тей, это ограничение можно обойти путем преобразования зависимостей в массивы. Потребители, которые полагаются на последовательности зависимостей, могут быть самым интуитивно понятным применением составных экземпляров одной и той же абстракции. Но перед тем как мы полностью отойдем от данной темы, нам необходимо рассмотреть последний (и, возможно, слегка неожиданный) случай, когда в дело вступают составные экземпляры. Интеграция Decorator'ов В разделе 9.1.2 "Паттерны и принципы механизма перехвата" мы обсуждали то, насколько паттерн проектирования Decorator полезен при реализации сквозных сущностей. По определению Decorator'ы представляют собой составные типы одной и той же абстракции. У нас есть, по крайней мере, две реализации абстракции: сам Decorat or и вложенный в него тип. Если бы мы помещали Decorator'ы в стек, то у нас было бы еще больше реализаций. Это еще один пример составных регистраций одного и того же сервиса. В отличие от предыдущих разделов эти регистрации не являются концептуально равносильными, а зависят друг от друга. В следующем разделе я продемонстрирую вам два разных способа конфигурирования Unit y для работы с данным паттерном. С оздание обертки для именованного компонента Класс Breading – это обертка IIngredient . Этот класс использует паттерн Constructor Injection для получения того экземпляра, который ему необходимо обернуть: public Breading(IIngredient ingredient) Чтобы получить Cotoletta , вам необходимо будет вложить VealCutlet (еще один IIngredient ) в класс Breading . Поскольку вы уже знаете, как соединить именованные компоненты с аргументами конструктора, было бы вполне естественным сделать что-то аналогичное в следующем листинге. 569 Листинг 14-12: Создание обертки с помощью именованного компонента container.RegisterType Компонент Breading по умолчанию должен иметь тип IIngredient , поэтому вам нужно присвоить VealCutlet имя IIngredient , поскольку по умолчанию IIngredient должен быть только один. При регистрации компонента Breading вы еще раз используете InjectionConstructor , чтобы указать, как контейнер Unit y должен интегрировать аргумент конструктора ingredient класса Breading ResolvedParameter позволяет вам указать, что должен разрешаться именно первый параметр конструктора (и только он) и интегрироваться с именованным компонентом cutlet При разрешении IIngredient вы получаете экземпляр Breading , в который вложен VealCutlet Это общепринятый способ создания обертки для компонента, но в случае, когда нам не нужны вложенные компоненты, мы можем использовать более неявный метод. С оздание обертки для конкретного компонента Если нам никогда не нужно будет напрямую разрешать вложенный компонент, мы можем использовать более неявный способ создания для него обертки. Представьте себе, что вам никогда не придется разрешать VealCutlet напрямую в виде IIngredient . В тех случаях, когда вам нужен будет IIngredient , вам всегда хотелось бы получать Cotoletta В таком случае вообще нет никакой необходимости конфигурировать VealCutlet . Вместо этого вы можете воспользоваться преимуществами того факта, что Unity автоматически разрешает конкретные типы, даже если они не зарегистрированы: container.RegisterType Вы уже знаете, что вам нужно внедрить в экземпляр Breading именно VealCutlet , поэтому нет причин для неявного определения ResolvedParameter , когда вы можете напрямую передать ResolvedParameter . Когда вы отправите запрос контейнеру на разрешение IIngredient , ResolvedParameter будет автоматически разрешен в экземпляр VealCutlet , поскольку это конкретный класс. В связи с тем, что VealCutlet реализует IIngredient , он нам подходит. Несмотря на то, что вы не регистрировали компонент VealCutlet , вы все еще можете это сделать, если вам необходимо сконфигурировать другие аспекты, например, его стиль существования: container.RegisterType 570 В этом примере вы конфигурируете конкретный VealCutlet в виде Singleton, но, поскольку вы не планируете разрешать его в виде IIngredient , вы не преобразуете его в интерфейс. Все это превращает его в VealCutlet по умолчанию, а затем ResolvedParameter сможет должным образом его разрешить. Как вы уже видели в этом разделе, при конфигурировании Decorator'ов существует несколько возможных вариантов. Во всех этих вариантах используется класс InjectionConstructor . В отличие от Castle Windsor контейнер Unity не поддерживает Decorator'ы явным образом, что может быть немного удивительным, поскольку подобно Windsor контейнер Unity максимально поддерживает паттерн Decorator: в виде механизма перехвата. Создание перехватчиков В разделе 9.3.3 "Пример: перехват с помощью W indsor" вы видели пример того, как добавить в WCF-приложение обработчик ошибок и Circuit Breaker с помощью возможности динамического перехвата, предлагаемой Castle W indsor. В этом разделе мы сделаем то же самое с помощью Unity. Как показано на рисунке 14-8, добавление аспекта в Unity – процесс, включающий в себя несколько шагов. Рисунок 14-8: Шаги, которые включает в себя процесс добавления аспекта в Unity Основная часть работы заключается в разработке самого перехватчика, но после его создания мы должны добавить этот перехватчик в контейнер. Механизм перехвата – это расширение контейнера Unity, поэтому мы должны добавить это расширение и в контейнер, чтобы все это работало. В этом разделе мы будем сначала создавать перехватчики для обработчика ошибок и Circuit Breaker, а в конце сконфигурируем контейнер с помощью этих перехватчиков. Реализация перехватчика обработчика исключений Реализация перехватчика для контейнера Unity требует от нас реализации интерфейса IInterceptionBehavior . Листинг 14-13 демонстрирует, как реализовать стратегию обработки исключений, приведенную в главе 9. Эта конкретная реализация стратегии для контейнера Unity соответствует листингу 9-8, который приводился при описании Castle Windsor, и листингу 12-4, который приводился при описании Spring.NET. 571 Листинг 14-13: Реализация интерфейса IInterceptionBehavior , предназначенного для обработки исключений. 1. public class ErrorHandlingInterceptionBehavior : 2. IInterceptionBehavior 3. { 4. public IEnumerable 5. { 6. return Type.EmptyTypes; 7. } 8. public bool WillExecute 9. { 10. get { return true; } 11. } 12. public IMethodReturn Invoke( 13. IMethodInvocation input, 14. GetNextInterceptionBehaviorDelegate getNext) 15. { 16. var result = getNext()(input, getNext); 17. if (result.Exception is CommunicationException 18. || result.Exception is 19. InvalidOperationException) 20. { 21. this.AlertUser(result.Exception.Message); 22. return input.CreateMethodReturn(null); 23. } 24. return result; 25. } 26. private void AlertUser(string message) 27. { 28. var sb = new StringBuilder(); 29. sb.AppendLine("An error occurred."); 30. sb.AppendLine("Your work is likely lost."); 31. sb.AppendLine("Please try again later."); 32. sb.AppendLine(); 33. sb.AppendLine(message); 34. MessageBox.Show(sb.ToString(), "Error", 35. MessageBoxButton.OK, 36. MessageBoxImage.Error); 37. } 38. } Строка 4-7: Передача интерфейсов Строка 8-11: Подключение перехватчика Строка 12-14: Реализация логики перехвата Строка 16: Получение результата из вложенного объекта Строка 17-22: Обработка исключений Класс ErrorHandlingInterceptionBehavior реализует IInterceptionBehavior , который является интерфейсом с тремя членами. Двое из этих членов, в основном, относятся к инфраструктуре Unity, и реализовать их довольно легко. Метод GetRequiredInterfaces позволяет нам указать, к каким интерфейсам обращается этот перехватчик, но возвращая пустой массив, вы можете отложить принятие решения до того момента, когда вы будете конфигурировать, какие компоненты собираетесь перехватывать. Свойство WillExecute должно возвращать true , если вы хотите, чтобы перехватчик работал. Это дает нам 572 возможность сконфигурировать, должен ли выполняться конкретный перехватчик. Но в этом примере вы хотите всегда выполнять ErrorHandlingInterceptionBehavior , если он сконфигурирован для компонента. Основная реализация IInterceptionBehavior выполняется в методе Invoke , который вызывается (так!) контейнером Unity при вызове перехватываемого компонента. Параметр input содержит некоторую информацию о текущем вызове метода, тогда как параметр getNext содержит делегат, который можно использовать для вызова вложенного компонента. Это приблизительно соответствует методу Procced , который используется в Castle W indsor и продемонстрирован в листинге 9-8. Вызов метода getNext дает вам метод Invoke , представляющий собой метод, который вложен в перехватчика. Это может быть другой перехватчик или сам вложенный компонент. Вызывая этот метод Invoke с первоначальными параметрами, вы получаете результат вложенного метода. Если метод возвращает одно из тех исключений, которые необходимо обработать, то вы предупреждаете об этом пользователя путем вывода на экран диалогового окна. В случае получения обрабатываемого исключения вам хотелось бы пресечь его сразу же после того, как вы предупредите пользователя. Если бы вложенный метод возвращал result , то это позволило бы исключению распространиться, поэтому вместо этого вы создаете новое возвращаемое значение и возвращаете его. В противном случае при отсутствии исключений вы вернули бы result из вложенного метода. ErrorHandlingInterceptionBehavior обрабатывает определенные исключения вложенного компонента. В роли этого компонента может выступать перехватчик в форме Circuit Breaker. Ре ализация Circuit Breaker перехватчика Circuit Breaker перехватчик – слегка более сложный перехватчик, поскольку для него необходима зависимость ICircuitBreaker . Но как демонстрирует следующий листинг, мы решаем эту проблему путем применения стандартного паттерна Constructor Injection. Когда дело доходит до компоновки класса, контейнер Unit y выполняет ее так же, как и любой другой компонент: пока он может разрешать зависимость, все идет нормально. Листинг 14-14: Реализация IInterceptionBehavior в виде Circuit Breaker 1. public class CircuitBreakerInteceptionBehavior : 2. IInterceptionBehavior 3. { 4. private readonly ICircuitBreaker breaker; 5. public CircuitBreakerInteceptionBehavior( 6. ICircuitBreaker breaker) 7. { 8. if (breaker == null) 9. { 10. throw new ArgumentNullException("breaker"); 11. } 12. this.breaker = breaker; 13. } 14. public IMethodReturn Invoke(IMethodInvocation input, 15. GetNextInterceptionBehaviorDelegate getNext) 16. { 17. try 573 18. { 19. this.breaker.Guard(); 20. } 21. catch (InvalidOperationException e) 22. { 23. return 24. input.CreateExceptionMethodReturn(e); 25. } 26. var result = getNext()(input, getNext); 27. if (result.Exception != null) 28. { 29. this.breaker.Trip(result.Exception); 30. } 31. else 32. { 33. this.breaker.Succeed(); 34. } 35. return result; 36. } 37. public IEnumerable 38. { 39. return Type.EmptyTypes; 40. } 41. public bool WillExecute 42. { 43. get { return true; } 44. } 45. } Строка 17-25: Реализация граничного оператора Строка 26: Возвращение result из вложенного метода Строка 27-30: Обработка исключения Строка 31-34: Обозначение успешного выполнения Строка 37-44: Необходимые данные для IInterceptionBehavior CircuitBreakerInteceptionBehavior должен делегировать свою реализацию экземпляру ICircuitBreaker . Поскольку Unit y, как и любой другой компонент, будет автоматически интегрировать перехватчик, для внедрения ICircuitBreaker можно воспользоват ься стандартным паттерном Constructor Injection. В методе Invoke вам необходимо реализовать идиоматическое выражение Guard- Succeed/Trip, которое вы уже видели в листингах 9-4 и 9-9. Для начала вам необходимо вызвать метод Guard , и вернуть исключение, если этот метод возвращает исключение. Unity предполагает, что вы будете передавать исключения не посредством вывода их, а путем инкапсуляции их в экземпляры IMethodReturn . Поэтому вы должны явным образом перехватывать InvalidOperationException и из перехваченного исключения создавать возвращаемое значение. Если метод Guard уже выполнился, то вы можете приступить к вызову вложенного метода. Делается это точно так же, как вы делали это в листинге 14-13. После получения result из вложенного метода можно проанализироват ь это значение, чтобы определить, не было ли возвращено исключение. Если это так, то с помощью метода Trip вы 574 устанавливаете прерыватель, но, обратите внимание на то, что при этом вы не изменяете result . Не забудьте о том, что после вызова метода Trip все равно необходимо выдавать исключение, поэтому result вы можете оставить неизмененным – он уже инкапсулирует исключение. При отсутствии исключений вы можете аналогичным образом указать на это прерывателю, вызвав метод Succeed . Не забудьте о том, что это может привести к закрытию другого открытого прерывателя. После реализации ErrorHandlingInterceptionBehavior и CircuitBreakerInteceptionBehavior настает время для того, чтобы сконфигурировать контейнер таким образом, чтобы ErrorHandlingInterceptionBehavior и CircuitBreakerInteceptionBehavior стали обертками для IProductManagementAgent Конфигурирова ние механизма перехвата Все, что нам необходимо сделать, – перехватить компонент IProductManagementAgent с помощью Circuit Breaker и механизма обработки ошибок таким образом, чтобы при возникновении исключений во время взаимодейст вия с веб-сервисом открывался Circuit Breaker и обрабатывалось исключение. Это дает приложению возможность восстанавливаться при каждом последующем подключении веб-сервиса или сети. Сначала нам необходимо добавить в контейнер Unity возможность поддержки механизма перехвата. Несмотря на то, что механизм перехвата является частью модуля приложений контейнера Unit y, реализуется он в отдельной сборке и, поэтому его необходимо добавлять явным образом. П римечание Чтобы иметь возможность использовать механизм перехвата в рамках контейнера Unit y, необходимо добавить ссылку на сборку Microsoft.Practices.Unity.InterceptionExtension После добавления ссылки на сборку Microsoft.Practices.Unity.InterceptionExtension необходимо добавить в контейнер расширение Interception . Это всего лишь еще одно расширение контейнера, поэтому добавить его можно с помощью метода AddNewExtension : container.AddNewExtension Несмотря на то, что приведенный код в принципе позволяет добавить в контейнер возможность использования механизма перехвата, нам все равно нужно добавить в компонент IProductManagementAgent необходимые перехватчики. На рисунке 14-9 продемонстрирована та конфигурация, к которой мы стремимся. Рисунок 14-9: IProductManagementAgent должен быть вложен в Circuit Breaker перехватчик таким образом, чтобы при выдаче агентом исключения на некоторое время открывался цикл. Circuit Breaker не управляет исключениями, а всего лишь регистрирует их. Поэтому за управление исключениями отвечает перехватчик обработки ошибок, который должен располагаться как можно дальше для того, чтобы иметь возможность управлять исключениями, получаемыми как от агента, так и от Circuit Breaker. 575 При конфигурировании механизма перехвата для компонента используется тот факт, что все перегрузки метода RegisterType принимают в качестве параметра массив объектов InjectionMember . До настоящего момента вы использовали только класс InjectionConstructor , но для конфигурирования механизма перехвата можно использовать совокупность и других классов: container.RegisterType Разобъем этот код на составляющие части. Сначала вы добавляете Interceptor . Это InjectionMember , который сообщает Unity о том, что далее следуют один или несколько InterceptionBehavior , которые выполняют перехват на уровне инетрфейса (в противоположность, например, перехвату виртуальных членов). Следующие два InjectionMember добавляют реализованные вами перехватчики ErrorHandlingInterceptionBehavior и CircuitBreakerInteceptionBehavior . Обратите внимание, что, поскольку вы сначала указываете ErrorHandlingInterceptionBehavior , он становится самым дальним перехватчиком и, в свою очередь, перехватывает CircuitBreakerInteceptionBehavior В конце этого примера кода вам необходимо убедиться, что выполнены требования всех зависимостей. Поскольку для CircuitBreakerInteceptionBehavior необходим ICircuitBreaker , вы должны зарегистрировать этот компонент: container.RegisterType Для обеспечения эффективност и важно, чтобы в коде присутствовал только один экземпляр Circuit Breaker (хотя бы по одному на каждый внешний ресурс), поэтому мы регистрируем его как Singleton. Кроме того, для конструктора CircuitBreaker мы задаем одноминутную задержку, чтобы гарантировать, что приложению разрешается восстанавливать прерванное подключение один раз в минуту. Этот раздел продемонстрировал, как можно применять механизм динамического перехвата в рамках контейнера Unity. Лично я считаю, что сложность использования 576 механизма перехвата в рамках Unity сравнима с поддержкой этого механизма контейнерами Castle W indsor и Spring.NET. Несмотря на то, что это применение не является полностью тривиальным, пользу оно приносит колоссальну ю. Механизм перехвата – это динамическая реализация паттерна Decorator, а паттерн Decorator сам по себе является совместным применением составных компонентов одного и того же типа. Контейнер Unity позволяет нам использовать различные подходы при работе с составными компонентами. Мы можем регистрировать компоненты в виде альтернатив друг другу, в виде пиров, которые разрешаются в виде последовательностей, в виде иерархических Decorator'ов или даже в виде перехватчиков. Что касается массивов, то тут Unity сам поймет, как ему поступать дальше, а остальные типы последовательност ей мы можем преобразовывать в массивы. Это также позволяет нам явным образом определять способ компоновки сервисов в случае, когда нам нужен более явный контроль. Кроме того, это может происходить в ситуации, когда нам нужно иметь дело с API, отклоняющимися от Constructor Injection. До настоящего момента вы наблюдали за тем, как конфигурировать экземпляры, включая то, как определять стили существования и как работать с составными компонентами. Но до этого момента мы позволяли контейнеру подключать зависимости, явным образом предполагая, что все компоненты используют Constructor Injection. Поскольку это не всегда происходит именно так, в следующем разделе мы сделаем краткий обзор того, как работать с классами, экземпляры которых должны создаваться особым образом. 577 14.4. Конфигурирование сложных API До настоящего момента мы рассматривали то, как можно конфигурировать компоненты, использующие Constructor Injection. Одним из главных преимуществ Constructor Injection является то, что DI-контейнеры, например, Unit y, могут с легкостью понимать, как компоновать и создавать все классы диаграммы зависимостей. Все становится менее понятным, когда API не столь хорошо функционируют. В этом разделе вы увидите, как работать с простейшими аргументами конструктора, статическими фабриками и Property Injection. Все это требует особого внимания. Начнем с рассмотрения классов, конструкторы которых принимают в качестве параметров простейшие типы, например, строки и целые числа. Конфигурирование простейших зависимостей Пока мы внедряем абстракции в потребителей, все в порядке. Но данный процесс усложняется, если конструктор зависит от простейшего типа, например, строкового, числового или перечисляемого. Наиболее часто это случается в реализациях доступа к данным, которые принимают в качестве параметра конструктора строку соединения. Но в то же время это является более общей проблемой, касающейся всех строковых и числовых типов. В сущности, регистрация строкового или числового типа в качестве компонента контейнера не имеет особого смысла. Но в рамках Unit y это, по крайней мере, осуществимо. Рассмотрим в качестве примера приведенный ниже конструктор: public ChiliConCarne(Spiciness spiciness) В этом примере Spiciness имеет перечисляемый тип: public enum Spiciness { Mild = 0, Medium, Hot } П редупреждение Согласно эмпирическому правилу перечисления являются code smell'ами и их нужно преобразовывать в полиморфные классы (имеющие разное состояние). Тем не менее, для данного примера они вполне нам подходят. Если вы хотите, чтобы все потребители Spiciness использовали одно и то же значение, можно зарегистрировать Spiciness и ChiliConCarne независимо друг от друга: container.RegisterInstance(Spiciness.Medium); container.RegisterType 578 Когда вы впоследствии будете разрешать ChiliConCarne , его Spiciness будет иметь значение Medium , как и все остальные компоненты, зависимые от Spiciness Если вы будете достаточным образом контролировать взаимосвязь Spiciness и ChiliConCarne , то сможете таким же образом поступить и с вездесущим InjectionConstructor , передав значение в конструктор: container.RegisterType Ранее в этой главе InjectionConstructor в основном использовался с ResolvedParameter , но можно поступить и по-другому: передать значение, которое затем будет передано прямо в конструктор компонента. В приведенном примере вы передаете значение Spiciness.Hot , которое затем будет передано прямо в конструктор ChiliConCarne , возвращая значение Hot Оба описанных здесь варианта стимулируют автоматическую интеграцию на предоставление конкретного значения для компонента. Если вам нужна более строго типизированная конфигурация, которая вызывает конструктор или статическую фабрику, вы также сможете это сделать. Регистрация компонентов с помощью блоков кода Еще один вариант создания компонента с примитивным значением – использовать еще один InjectionMember , позволяющий передавать делегат, который создает компонент: container.RegisterType InjectionFactory – это еще один класс, унаследованный от абстрактного класса InjectionMember . Он обладает двумя перегруженными конструкторами, но мы используем самый простой из них, который в качестве входных данных принимает Func . Это дает нам возможность определить блок кода, с помощью которого будет создаваться компонент. В этом примере всякий раз при разрешении компонента ICourse будет вызываться конструктор ChiliConCarne с параметром Spiciness.Hot П римечание Блок кода, приведенный в примере выше, полностью идентичен соответствующему блоку кода, приведенному для контейнера Autofac в разделе 13.4.2 "Регистрация объектов с помощью блоков кода". Когда дело касается класса ChiliConCarne , вам предоставляется выбор между автоматической интеграцией и использованием блока кода. Но другие классы более ограничены: их экземпляры нельзя создать с помощью открытого конструктора. Чтобы создать экземпляры типа, вместо открытого конструктора вам приходится использовать некоторого рода фабрику. Для DI-контейнеров это всегда проблематично, поскольку по умолчанию им нужны открытые конструкторы. Рассмотрим приведенный ниже пример конструктора открытого класса JunkFood : 579 internal JunkFood(string name) Даже если класс JunkFood является открытым, конструктор расположен внутри него. Очевидно, экземпляры JunkFood должны создаваться с помощью статического класса JunkFoodFactory : public static class JunkFoodFactory { public static IMeal Create(string name) { return new JunkFood(name); } } С точки зрения Unit y, это проблемное API, поскольку в нем отсутствуют точно выраженные и заданные соглашения касательно статических фабрик. Т ут требуется помощь – и мы можем предоставить ее посредством блока кода, который Autofac может исполнять для того, чтобы создать экземпляр: container.RegisterType В этот раз вы используете класс InjectionFactory для создания компонента, вызывая статическую фабрику в рамках блока кода. Всякий раз при разрешении IMeal будет вызываться JunkFoodFactory.Create и возвращаться результат. Является ли написание блока кода для создания экземпляра лучшим вариантом, нежели прямой вызов кода? При использовании блока кода внутри конструктора InjectionFactory мы приобретаем следующие преимущества: IMeal преобразуется в JunkFood Стиль существования остается доступным для конфигурирования. Несмотря на то, что для создания экземпляра вызывается блок кода, он может и не вызываться всякий раз при запросе экземпляра. По умолчанию он вызывается, но если мы изменим стиль существования на Singleton, то блок кода будет вызыват ься только один раз, а результат будет кэшироваться и впоследствии повторно использоваться. Последним рассматриваемым нами отклонением от Constructor Injection является Propert y Injection. Интегрирование с помощью Property Injection Property Injection – это менее определенная форма механизма внедрения зависимостей, поскольку компилятор не принудает нас задавать значение свойства, доступного для записи. Это касается и Unit y, который будет оставлять доступные для записи свойства незаполненными до тех пор, пока мы явно не попросим его заполнить их. Рассмотрим класс CaesarSalad : public class CaesarSalad : ICourse { public IIngredient Extra { get; set; } } 580 По всеобщему заблуждению в состав салата "Цезарь" входит курица. По своей сути "Цезарь" является салатом, но, поскольку с курицей он вкуснее, то ее часто предлагают использовать в нем в качестве дополнительного ингредиента. Класс CaesarSalad моделирует такую возможность посредством доступного для записи свойства под названием Extra Если вы конфигурируете только класс CaesarSalad , явно не обращаясь к свойству Extra , то этому свойству не будет присвоено значение. Вы все равно можете разрешать экземпляр, но свойство Extra будет иметь значение по умолчанию, которое ему присвоил конструктор (если только это имеет место). Помимо этого, чтобы присвоить значение свойству, вы можете использовать InjectionMember : container.RegisterType Т очно так же, как вы можете использовать класс InjectionConstructor для конфигурирования Constructor Injection, вы можете использовать класс InjectionProperty для конфигурирования Propert y Injection. InjectionProperty – это еще один класс, унаследованный от InjectionMember . Чтобы его использовать, вы должны указать имя того свойства, которое необходимо заполнить. В этом примере вы собираетесь заполнить свойство Extra . Это приведет к автоматической интеграции свойства, поэтому очень важно, чтобы Unity умел разрешать этот тип. Свойство Extra имеет тип IIngredient , поэтому Unit y разрешит это свойство в Chicken благодаря тому, что ранее вы зарегистрировали Chicken как IIngredient П редупреждение Если для свойства задается паттерн Propert y Injection, то Unity должен уметь разрешать тип этого свойства. Если он не сможет это сделать, то при попытке разрешения такого типа будет возникать исключение. Когда вы на основании этой регистрации разрешаете ICourse , вы получаете экземпляр CaesarSalad , свойству Extra которого присвоен экземпляр Chicken В предыдущем примере использовался IIngredient по умолчанию, но для того чтобы присвоить значение свойству, мы можем использовать и другую перегрузку класса InjectionProperty . М ы можем присвоить ему непосредственное значение или можем воспользоваться проверенным классом ResolvedParameter , чтобы сослаться на именованный компонент: container.RegisterType Экземпляр ResolvedParameter ссылается на ранее зарегистрированный компонент chicken , гарантируя, что при разрешении ICourse вы получите CaesarSalad , свойству Extra которого присвоен экземпляр Chicken 581 В этом разделе вы увидели, как можно использовать Unity для работы с более трудными API разработки. Для указания конкретных экземпляров или блоков кода, которые будут использоваться для создания экземпляро в, можно применять различные классы, унаследованные от InjectionMember , а также конфигурировать Propert y Injection. Я считаю, что это API становится довольно простым для изучения, как только вы осознаете его смысл. 582 14.5. Резюме Unity – это DI-контейнер, разрабатываемый группой Patterns&Practices компании Microsoft. Несмотря на то, что он не является как таковым решением компании Microsoft , многие организации, занимающиеся разработкой приложений, все еще считают его некоторого рода полуофициальным продуктом компании Microsoft. Теми компаниями, которые строго соблюдают принцип использования только продуктов компании Microsoft, этот факт может восприниматься как преимущество, поскольку в Unity часто входят модули приложений группы Patterns&Practices. Вторым фактором, влияющим на их выбор, является наличие официальной документации. API, предлагаемое контейнером Unity, является постоянным, но, в отличие от большинства других контейнеров, не гибким. Иногда это может приводить к усложнению проверки, но как только вы поймете суть контейнера Unity, вы сможете оценить его постоянность: за исключением необязательных имен и LifetimeManager 'ов, вся дальнейшая конфигурация выполняется с помощью классов, унаследованных от абстрактного класса InjectionConstructor Помимо императивного API контейнер Unity также обладает комплексной XML-схемой, которая позволяет определять конфигурацию контейнера как в XML, так и в коде. С другой стороны, он не поддерживает основанный на соглашениях механизм автоматической регистрации. Unity – один из тех относительно немногих DI-контейнер'ов, которые поддерживают механизм перехвата. Эта поддержка обеспечивается за счет расширения основного контейнера, но при этом является частью модуля приложений. Одной из относительно слабых сторон контейнера Unit y является механизм управления жизненным циклом. Несмотря на то, что встроенные стили существования кажутся вполне достаточным набором стилей, управление ресурсами становится довольно трудным процессом, поскольку после прекращения использования компоненты не высвобождаются должным образом. К счастью, не все так уж плохо, поскольку мы можем реализовать пользовательские стили существования, которые будут решать эти проблемы. Получается, что управлять жизненными циклами в рамках контейнера Unity достаточно сложно, но вполне возможно. В общем Unity является довольно добротным DI-контейнер'ом. Он имеет свои слабые стороны, но, с другой стороны, предоставляет довольно полный набор возможностей. Единственная важная упущенная возможность – автоматическая регистрация, но поскольку API контейнера Unity является открытым, при желании его можно усовершенствовать. Эта глава познакомила нас с полуофициальным DI-контейнер'ом компании Microsoft . В следующей главе мы рассмотрим контейнер, слегка отличающийся от контейнера Unit y. Некоторые вообще не считают его DI-контейнер'ом. С другой стороны, он входит в состав стандартной библиотеки классов .NET 4 и имеет множество схожих с DI-контейнер'ами черт. Поэтому стоит все-таки рассмотреть Managed Extensibility Fram ework (MEF – управляемая платформа расширений). 583 15. MEF Меню: Знакомство с M EF Управление жизненным циклом Работа с составными компонентами Конфигурирование сложных API В пяти предыдущих главах вы наблюдали за тем, как можно использовать различные DI- контейнер'ы в качестве средств реализации паттернов и принципов, описанных в остальной части книги. В этой главе мы будем заниматься несколько другим, поскольку Managed Extensibility Fram ework (MEF – управляемая платформа расширений) в действительности не является DI-контейнер'ом. Судя по названию, MEF – это фреймворк, предназначенный для решения проблем расширяемости приложений. Целью MEF является подключение к стандартному приложению дополнительных сценариев. Вероятно, Visual Studio 2010 является самым первым и наиболее известным приложением, в котором MEF используется для поддержки плагинов. Но с другой стороны, любое другое приложение, созданное на платформе .NET 4 или Silverlight 4, также может использовать MEF для раскрытия возможностей расширяемости. Если MEF не является DI-контейнером, то почему тогда на ее рассмотрение мы выделили целую главу? Самая важная причина заключается в том, что MEF наст олько похож на DI- контейнер, что вы потратите уйму времени на то, чтобы разобраться, в чем же его отличие от реального DI-контейнера. Поскольку он является частью .NET 4 или Silverlight 4, возможно, использование его в качестве DI-контейнера было бы довольно заманчивым, если бы вы не понимали тонкой границы между ними. Цель этой главы – выявить эти различия для того, чтобы вы могли принять верное решение. П римечание Не забывайте, что если вам не интересен MEF и вы уже решили использовать другой DI- контейнер, вы всегда можете пропустить эту главу. |