Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
Самый быстрый маршрут на основе имеющихся видов транспорта? Самый живописный маршрут? Каждый вариант представляет собой отдельный алгоритм, и приложение может обрабатывать каждый алгоритм как абстракцию, поэтому оно обрабатывает алгоритмы одинаково. Для расчета маршрута приложению нужен алгоритм маршрута, и это не важно, какой именно. Мы должны сказать ему, какой алгоритм он должен использовать, но мы этого не узнаем до времени выполнения, потому что он основан на выборе пользователя. В этом разделе рассказывается, как мы можем решить такого рода проблему. Прежде чем перейти к примеру, мы кратко поговорим об общей проблеме. Когда мы закончим, вашей рефлекторной реакцией на этот вызов должен быть ввод абстрактной фабрики. Абстракции с зависимостями времени выполнения Когда мы используем внедрение в конструктор, мы неявно утверждаем – мы ожидаем, что зависимость должна быть однозначной во время выполнения. Рассмотрим сигнатуру конструктора, как эта: Рисунок 6-1a: Это никогда не будет работать, если во время выполнения неясно, какая реализация DiscountRepository должна быть использована. Во время разработки мы можем рассматривать зависимость как абстракцию и следовать Принципу подстановки Барбары Лисков (Liskov Substit ution Principle), но во время выполнения решение о том, какой DiscountRepository использовать, должно быть принято до того, как будет создан RepositoryBasketDiscountPolicy . Поскольку зависимость запрашивается через конструктор, мы не можем принять решения после этого момента. Это лишь означает, что как только класс RepositoryBasketDiscountPolicy идет в работу, не может быть никакой двусмысленности относительно DiscountRepository . Прочие потребители также могут запрашивать экземпляр ы DiscountRepository , и будут ли они все получать одинаковые или разные экземпляры, имеет меньшее значение. Т акие зависимости часто представляют сервисы, а не доменные объекты. Концептуально, есть только один экземпляр данного сервиса. 196 Примечание Как вы увидите в главе 9, может быть несколько реализаций одной и той же абстракции в работе в одно и то же время. Однако, с точки зрения потребителя, есть только одна. Сервисы принадлежат к общей группе зависимостей, но иногда зависимость представляет надлежащий доменный объект. Это особенно верно, когда речь идет о меняющих поведение абстракциях, такие как Стратегии (Strategies). Предыдущий алгоритм расчета маршрута является одним из таких примеров. Другим может быть коллекция графических редакторов для растровых эффектов: каждый эффект выполняет преобразование растрового изображения, но все они могут быть раскрыты в приложении как абстракции – это также архитектура, позволяющая поддерживать надстройки. В таких случаях мы не можем запросить зависимость через конструктор, потому что Composer не будет знать, какую реализацию выбрать. В игре может быть ноль, один или много экземпляров в разное время жизненного цикла приложения. Зависимость неоднозначна во время разработки. Как всегда, в разработке программного обеспечения, решением является окольный путь: на этот раз, абстрактная фабрика. Абстрактная фабрика Паттерн проектирования абстрактная фабрика (Abstract Factory) решает проблему, когда мы по желанию должны быть в состоянии запросить экземпляр абстракции. Он предлагает мост между абстракций и конкретными значениями времени выполнения, что позволяет нам переводить значение времени выполнения в зависимость. На следующем рисунке показано, как это работает, то есть, вводится новая абстракция, которая создает экземпляры первоначально требуемой абстракции. Рисунок: Если мы должны быть в состоянии создать экземпляр ы IFoo по запросу, нам нужен способ сделать это. Абстрактная фабрика – это другая абстракция, которую мы можем использовать для создания таких экземпляров по мере необходимости. Абстрактная фабрика – сама абстракция, единственной целью которой является создание экземпляров первоначально требуемых абстракции. Если мы должны быть в состоянии создать экземпляр ы IFoo из конкретных экземпляров Bar , соответствующая абстрактная фабрика может выглядет ь следующим образом: public interface IFooFactory { IFoo Create(Bar bar); } В ухудшенном варианте абстрактная фабрика может не принимать никаких входных параметров: 197 public interface IFooFactory { IFoo Create(); } В таком случае абстрактная фабрика становится чистой фабрикой, в то время как аспект преобразования исчезает. Абстрактная фабрика является одним из наиболее полезных паттернов проектирования. Имейте это в виду, потому что она может быть использована для решения многих вопросов, связанных с DI. С овет Когда один или несколько параметров, переданных абстрактной фабрике, сами по себе являются абстракциями, эта техника также становится примером внедрения в метод. Абстрактная фабрика является универсальным решением, когда мы должны создать зависимости из значения времени выполнения. П рое ктные требования Насколько полезной может быть абстрактная фабрика, настолько осторожно мы должны применять ее. Зависимости, созданные абстрактной фабрикой, должны концепт уально требовать значению времени выполнения. Переход от значения времени выполнения в абстракцию должен иметь смысл на концептуально м уровне. Если вы чувствуете желание ввести абстрактную фабрику, чтобы иметь возможность создавать экземпляры конкретной реализации, стоит воспользоваться протекающей абстракцией (Leaky Abstraction). П ротекающие абстракции Т ак же, как разработка через тестирование (Test-Driven Development , T DD) обеспечивает тестируемость, безопаснее сначала определить интерфейсы, а затем дальнейшу ю программу для них. Тем не менее, бывают случаи, когда у нас уже есть конкретный тип и теперь мы хотим извлечь интерфейс. Когда мы это делаем, мы должны позаботиться о том, чтобы лежащая в основе реализация не протекала. Один из моментов, когда это может случиться, если мы только извлекаем интерфейс из данного конкретного типа, но все параметры и возвращаемые типы по- прежнему являются конкретными типами, определенными в той же библиотеке. Если нам нужно извлечь интерфейс, мы должны делать это рекурсивном образом, гарантируя, что все типы, извлекаемые корневым интерфейсом, сами являются интерфейсами. Я называю это глубоким извлечением (Deep Extraction), а результат – глубокими интерфейсами (Deep Interfaces). В ASP.NET MVC есть некоторые примеры извлечения глубоких интерфейсов. Например, у HttpContextBase есть свойство Request типа HttpRequestBase , и так далее. Эта абстракция была рекурсивно извлечена из System.Web.HttpContext Всегда учитывайте, имеет ли данная абстракция смысл для других реализаций, нежели той, которую вы имеете в виду. Если это не так, вы должны пересмотреть вашу структуру. 198 Абстрактные фабрики бывают разных форм и обличий, и не всегда может быть очевидным, что они у вас есть. П римечание Любая абстракция, которая создает экземпляры других абстракций, является фабрикой абстракций (абстрактной фабрикой). И для этого не нужно иметь имя, которое заканчивается на Factory. Давайте посмотрим на пару примеров: сначала простой, идиоматический пример, а впоследствии более сложный пример, где абстрактная фабрика скрыта под другим именем. Пример: выбор алгоритма маршрута Во введении к этому разделу кратко обсуждался сайт с онлайн картой, где пользователь может выбрать различные алгоритмы расчета маршрута. В этом разделе мы рассмотрим то, как применить абстрактную фабрику, чтобы удовлетворить этому требованию В веб-приложениях вы можете только передать примитивные типы от браузера на сервер, поэтому когда пользователь выбирает алгоритм маршрута из выпадающего списка, вы должны представить это как число или строку. Перечисление – это на самом деле просто число, так что на сервере можно представить выбор с помощью RouteType : public enum RouteType { Shortest = 0, Fastest, Scenic } Однако то, что вам нужно, это экземпляр IRouteAlgorithm , который может рассчитать маршрут для вас. Для перехода от значения времени выполнения RouteType к IRouteAlgorithm , вы можете определить абстрактную фабрику: public interface IRouteAlgorithmFactory { IRouteAlgorithm CreateAlgorithm(RouteType routeType); } Это позволяет реализовать метод GetRoute для RouteController путем внедрения IRouteAlgorithmFactory и использоват ь это для перевода значения времени выполнения в нужную зависимость: IRouteAlgorithm . Следующий листинг демонстрирует взаимодейст вие. Листинг 6-1: Использование IRouteAlgorithmFactory 1. public class RouteController 2. { 3. private readonly IRouteAlgorithmFactory factory; 4. public RouteController(IRouteAlgorithmFactory factory) 5. { 6. if (factory == null) 7. { 8. throw new ArgumentNullException("factory"); 199 9. } 10. this.factory = factory; 11. } 12. public IRoute GetRoute(RouteSpecification spec, 13. RouteType routeType) 14. { 15. IRouteAlgorithm algorithm = 16. this.factory.CreateAlgorithm(routeType); 17. return algorithm.CalculateRoute(spec); 18. } 19. } Строки 15-16: Преобразование значения времени выполнения Строка 17: Использовать преобразованный алгоритм Ответственност ь класса RouteController заключается в обработке веб запросов. Метод GetRoute получает спецификацию пользователя о пунктах отправления и назначения, а также выбранный тип маршрута (через RouteType ). Вам нужна абстрактная фабрика для преобразования значения времени выполнения RouteType в экземпляр IRouteAlgorithm , поэтому вы запрашиваете экземпляр IRouteAlgorithmFactory , используя стандартное внедрение в конструктор. В методе GetRoute вы можете использовать factory для преобразования переменной routeType в IRouteAlgorithm . Когда это будет сделано, вы можете использовать это для расчета маршрута и возвращения результата. П римечание Для краткости я опустил ограждающее условие в методе GetRoute . Т ем не менее, предоставляемый RouteSpecification может быть null , поэтому в более совершенной реализации нужно сделать на это проверку. Наиболее очевидная реализация IRouteAlgorithmFactory будет включать простой оператор switch и возвращать три различные реализации IRouteAlgorithm на основе входных данных. Т ем не менее, я оставляю это как упражнение для читателя. Этот пример продемонстрировал переход значения времени выполнения к зависимости с использованием абстрактной фабрикой в чистом виде. Следующий пример показывает более сложный вариант, в котором с первого взгляда вы можете даже и не понять, что используется абстрактная фабрика. Пример: использование CurrencyProvider В большей части главы 4 вы видели, как реализовать конвертацию валюты в контроллере ASP.NET MVC. Тип Currency является абстрактным классом, воспроизведенным здесь, чтобы вам не пришлось возвращаться в раздел 4.1.4: public abstract partial class Currency { public abstract string Code { get; } public abstract decimal GetExchangeRateFor(string currencyCode); } 200 На первый взгляд, кажется немного странным, обрабатывать такое понятие, как валюта, в качестве абстракции, потому что оно звучит скорее как Объект-значение (Value Object). Т ем не менее, обратите внимание, что метод GetExchangeRateFor позволяет нам запросить его для практически неограниченного множества конверсий. Предположим, есть 100 курсов обмена, каждый экземпляр Currency будет потреблять больше, чем 2 КБ памяти. Это вроде бы и не так много, но, возможно, потребуется оптимизация, например, использование паттерна проектирования Приспособленец (Flyweight). Другой вопрос, который сразу возникает при конвертации валюты, касается денег (sic!) валюты: другими словами, ее актуальности. Такие приложения, как трейдерское программное обеспечение для монетарных рынков, требуют того, чтобы курсы валют обновлялись несколько раз в секунду, в то время как международные коммерческие сайты, скорее всего, обойдутся несколькими обновлениями для стабильной валюты. Т акие приложения могут также включать разметку или стратегии округления, добавляя потенциальную сложность в реализацию типа Currency . В свете этого, абстрактный класс Currency кажется вполне разумным. Когда потребителю, например, ASP.NET MVC контроллеру, необходимо преобразовать цены, он затребует Currency как зависимость для выполнения конвертации. В примере коммерческого приложения, используемого в этой книге, у класса Money , который используются для представления цен, есть этот метод конверсии: public Money ConvertTo(Currency currency) Потребители, такие как контроллер, могут предоставить экземпляр Currency всем ценам, чтобы конвертировать их, но теперь возникает вопрос, какой экземпляр Currency ? Выбор целевого Currency зависит от значения времени выполнения: выбранной пользователем валюты. Это означает, что мы не можем запросить один объект Currency через внедрение в конструктор, потому что Com poser не сможет узнать, какую валюту использовать. Как вы видели в разделе 4.1.1, решением является внедрение CurrencyProvider вместо одного Currency : public abstract class CurrencyProvider { public abstract Currency GetCurrency(string currencyCode); } Рисунок 6-2 показывает, как контроллер обычно извлекает код предпочтительно й валюты пользователя из профиля и использует внедренный CurrencyProvider для создания соответствующего экземпляра Currency 201 Рисунок 6-2: Внедренный CurrencyProvider используется для отображения простого значения времени выполнения (строку кода валюты) в зависимость времени выполнения (экземпляр Currency ). Хотя он имеет другое имя, CurrencyProvider является абстрактной фабрикой, которая помогает нам преодолеть разрыв между значением времени выполнения и зависимостью времени выполнения. Currency концептуально зависит от кода валюты, поэтому мы можем быть уверены, что мы не внедрили протекающую абстракцию путем введения CurrencyProvider Еще один пример из главы 4 показывает дегенеративный случай, где нет никаких первоначальных входных параметров. В разделе 4.2.4 вы видели, как у абстрактного CurrencyProfileService есть метод GetCurrencyCode , который будет возвращать текущий валютный код пользователя: public abstract string GetCurrencyCode(); Хотя метод GetCurrencyCode возвращает строку, а не абстракцию, вы можете рассматривать CurrencyProfileService как вариант абстрактной фабрики. В HomeController вы объединяете оба варианта, чтобы выяснить предпочтительную валюту пользователя: var currencyCode = this.CurrencyProfileService.GetCurrencyCode(); var currency = this.currencyProvider.GetCurrency(currencyCode); И в CurrencyProfileService , и в currencyProvider внедряются абстрактные фабрики, которые доступны для любого члена класса HomeController . В разделах 4.1.4 и 4.2.4 показано, как они внедряются. Всякий раз, когда мы должны подготовить значение времени выполнения, и мы хотим иметь возможность варьировать средствами, с помощью которых мы создаем это значение независимо от потребителя, мы можем внедрить абстрактную фабрику. Как правило, это stateless (не сохраняющий состояние) сервис, поэтому он подходит лучше к тому способу, как мы обычно обрабатываем зависимости, и мы можем использовать внедрение в конструктор или внедрение в свойство, чтобы предоставить потребителю фабрику. Существует еще один тип сценариев, при котором абстрактная фабрика также обеспечивает хорошее решение. Это происходит, если мы должны работать с недолговечными зависимостями. 202 6.2. Работа с недолговечными зависимостями Некоторые зависимости кажутся концептуально недолговечными. Они обычно представляют соединения с внешними ресурсами, такими как базы данных или веб сервисы. Такие связи должны быть закрытыми или произойдут утечки ресурсов. В этом разделе мы рассмотрим лучший способ решения таких проблем. Как и в предыду щем разделе, мы начнем с изучения общего случая, а затем приступим к примеру. Когда мы закончим, вы должны понимать две вещи: Вы можете моделировать такие взаимодействия при помощи абстрактной фабрики, которая создает одноразовые экземпляры. Вы должны стремиться скрыть этот паттерн за stateless абстракцией. Прежде чем перейти к примеру, давайте посмотрим, что заставило меня сказать это. Закрытие соединений через абстракции Смысл слабой связанности и принцип подстановки Барбары Лисков состоит в том, что зависимость может быть реализована любым количеством способов. Даже тогда, когда вы имеете в виду конкретную реализацию, потенциально в будущем на ум может прийти радикально отличная реализация. Т ем не менее, некоторые зависимости представляют доступ к внешним ресурсам, а эти, как правило, переходят на вопросы, связанные с использованием ресурсов. Я, конечно, говорю о соединениях в различных видах и формах. Большинство .NET разработчиков знают, что они должны открыть соединение ADO.NET прямо перед его использованием и закрыть его снова, как только работа будет закончена. Современные API интерфейсы, как LINQ to SQL или LINQ to Ent ities, автоматически сделают это для нас так, поэтому мы не должны работать с этим напрямую. Хотя любой программист должен знать о правильно используемом паттерне, касающемся ADO.NET соединений, гораздо менее известно, что это же самое верно для WCF клиентов. Они должны быть закрыты, как только мы закончим с определенным набором операций или сервисов, потому что в противном случае они могут оставить «мертвые» ресурсы на стороне сервера. W CF сервисы и состояние Фундаментальное правило сервисной ориентации заключается в том, что сервисы не должны сохранять состояние (должны быть stateless). Если мы будем следовать этому правилу, тогда, безусловно, W CF клиент не оставит «живые» ресурсы на стороне сервера, так? Удивительно, но это не может быть не так. Даже если мы построим сервис полностью stateless, WCF может таким не быть. Это зависит от связывания. 203 Один из многих примеров относится к безопасности. Основанная на сообщениях безопасность, как правило, влияет на производительность. Это верно, потому что асимметричные ключи требуют большого объема вычислений, но это еще более верно для Federated security, потому что несколько обменов сообщениями участвуют в создании контекста безопасности. Поведением по умолчанию для WCF является создание защищенного диалога на основе обмена асимметричными ключами. Сервис и клиент используют «рукопожатие» асимметричной безопасности для обмена специальным симметричным ключом, который используется для обеспечения безопасности всех последующих сообщений, которые являются частью этой сессии. Однако такое поведение требует, чтобы обе стороны сохранили общий секрет в памяти. Клиент должен распрощаться с сервисом, когда он закончит сессию, или это будет «мертвый» симметричный ключ на сервере. Это, в конечном счете, может быть очищено после тайм-аута, но до тех пор это все занимает память. Чтобы сохранить ресурсы на сервере, клиент должен явно закрыть "соединение", когда оно будет завершено. Хотя это верно не для всех W CF связываний, но таких много, так что мы должны гарантировать, что наши клиенты WCF являются «хорошими ребятами». Как мы можем совместить необходимость закрыть WCF соединение с желанием избежать протекающей абстракции? Этот вопрос может быть рассмотрен на двух уровнях: Сокрытие всей логики управления соединениями за абстракцией Подражание открытию и закрытию соединений на более детальном уровне Первый вариант предпочтительнее, но иногда также требуется и второй. Оба варианта могут быть объединены, чтобы получить лучшее от обоих. С окрытие управления соединениями за абстракцией DI не является оправданием для написания приложений с утечками памяти, так что мы должны иметь возможность явно закрывать соединения как можно скорее. С другой стороны, любая зависимость может представлять или не представлять связь «вне процесса», так что у нас была бы протекающая абстракция, если бы мы должны были смоделировать абстракцию, чтобы она включала метод Close Некоторые люди прибегают к тому, что разрешают зависимостям быть наследованными от IDisposable . Тем не менее, метод Dispose – это просто метод Close с другим именем, так что такой подход не решит основной проблемы. К счастью, технологии доступа к базам данных, такие как LINQ to SQL и LINQ to Ent ities демонстрируют правильный подход. В обоих случаях мы имеем доступ к данным через контекст, который содержит соединение. Всякий раз, когда мы общаемся с базой данных, контекст автоматически открывает и закрывает соединение по мере необходимости, полностью освобождая нас от бремени борьбы с этим. Наша первая реакция должна заключаться в том, чтобы сделать то же самое. Рисунок 6-3 показывает, как определить абстракцию на уровне, который достаточно «крупнозернист», чтобы реализация могла открывать и закрывать соединения по мере необходимости. 204 Рисунок 6-3: Мы можем разработать интерфейс, который достаточно «крупнозернист», чтобы каждый метод включал в себя все взаимодействия с внешним ресурсом в одном пакете. Consum er вызывает метод для интерфейса IResource . Реализация этого метода может открыть соединение и вызывать несколько методов по отношению ко внешним ресурсам до закрытия соединения и возвращения результата потребителю. Потребитель никогда не знает, что некоторые реализации могут открывать и закрывать соединения от его имени. Когда это возможно, мы должны стремиться к разработке пользовательской зависимости, так чтобы нам никогда явно не нужно было иметь дело с жизненным циклом зависимости на этом уровне. Есть, однако, случаи, когда мы не можем этого сделать. О ткры тие и закрытие зависимостей Проблема с «крупнозернист ыми» API заключается в том, что они не могут быть достаточно гибкими. Иногда нужна просто абстракция, которая позволяет нам явно моделировать жизненный цикл зависимости, которая в противном случае приведет к утечке памяти. Внимание Остановка одной утечки приводит к другой. Мы меняем утечки памяти на "дырявые" абстракции (Leaky Abstractions). Наиболее распространенный жизненный цикл, который нам нужно смоделировать, показан на фигуре 6-4. Рисунок 6-4: Наиболее распространенный жизненный цикл соединения заключается в том, что мы создаем, используем и закрываем его, когда заканчиваем работу с ним. Это жизненный цикл, который мы должны смоделироват ь, если мы должны моделировать такие вещи. 205 В разделе 6-1 было показано, как использовать абстрактную фабрику для создания зависимости по желанию, так что мы должны найти идиому кодирования, которая подходит к закрытию соединения. Как показано на рисунке 6-4, мы можем использовать паттерн IDisposable , чтобы работать с зависимостями, использующими соединения. Внимание С одноразовыми зависимостями код «пахнет». Используйте их только тогда, когда нет другого выбора. Подробнее об этом в разделе 8.2. Другими словами, мы можем смоделировать почти любое взаимодействие, которое соответствует жизненному циклу из рисунка 6-4, при помощи абстрактной фабрики, которая создает одноразовые зависимости (см. рисунок 6-5). Рисунок 6-5: Мы можем смоделировать управление соединением и аналогичные жизненные циклы, принимая зависимость от абстрактной фабрики, такой как IFooFactory , показанной здесь. Каждый раз, когда потребителю нужен экземпляр IFoo , он создается IFooFactory , но потребитель должен помнить, что должен избавиться от него соответствующим образом. Паттерн, показанный на рисунке 6-5, часто лучше всего реализовать с помощью ключевого слова C# using (или аналогичной конструкции в других языках). Как покажет следующий пример, имеет смысл объединять оба подхода, которые мы только что обсуждали. Доступ к ресурсам моделируется как «крупнозернистая» абстракция, которая защищает потребителя от непосредственно й работы с управлением жизненным циклом, в то время как реализация использует описанное сочетание абстрактной фабрики и одноразовых зависимостей. Давайте посмотрим, как это работает. Пример: вызов сервиса управления продуктом Представьте себе Windows Presentation Foundation (WP F), который обеспечивает богатый пользовательский интерфейс для управления каталогом продукции. Т акое приложение может общаться с бэкэндом (сервером) через WCF сервис, который предоставляет необходимые операции по управлению каталогом продукции. На рисунке 6-6 показано, как реализация сочетает в себе обе техники из предыдущего раздела. 206 Рисунок 6-6: Класс MainWindowViewModel потребляет интерфейс IProductManagementAgent . Это крупнозернистый интерфейс, который предоставляет соответствующие методы для потребителя. С точки зрения MainWindowViewModel , нет никакого управления соединением. Когда приложение запущено, класс WcfProductManagementAgent обеспечивает реализацию крупнозернистого интерфейса. Он делает это, потребляя абстрактную фабрику IProductChannelFactory , которая создает одноразовые экземпляры. Интерфейс IProductManagementServiceChannel наследуется от IDisposable , что позволяет WcfProductManagementAgent избавиться от WCF клиента, когда операции были успешно вызваны. Примечание Мы вернемся к этому W PF приложению в разделах 6.3.2 и 7.4.2. Потребитель защищен от управления соединением, которое является частью реализации WcfProductManagementAgent Всякий раз, когда класс MainWindowViewModel хочет вызвать сервисную операцию, он вызывает зависимость IProductManagementAgent . Это совершенно нормальная зависимость, внедренная через конструктор. Это, например, показывает, как удалить продукт: this.agent.DeleteProduct(productId); В этом случае this.agent является внедренной зависимостью IProductManagementAgent Как видите, здесь нет никакого явного управления соединением, но если вы посмотрите на реализацию в WcfProductManagementAgent , вы увидите, как абстрактная фабрика используется в комбинации с одноразовой зависимостью: using (var channel = this.factory.CreateChannel()) { channel.DeleteProduct(productId); } У вас нет внедренного W CF клиента, который можно использовать для вызова сервисной операции, потому что вы должны закрыть клиент, как только вы с ним закончите, и это не возможно – повторно использовать WCF каналы. Вместо этого у вас есть внедренная абстрактная фабрика, которую вы используете, чтобы создать новый канал. Поскольку операция заключается в using , выход из зоны видимости отключает канал. 207 Зависимость factory является экземпляром интерфейса IProductChannelFactory . Это пользовательский интерфейс, созданный по данному случаю: public interface IProductChannelFactory { IProductManagementServiceChannel CreateChannel(); } Т ем не менее, интерфейс IProductManagementServiceChannel является автоматически сгенерированным интерфейсом, созданным вместе со всеми другими типами WCF прокси. Каждый раз, когда мы создаем ссылку на сервис в Visual Studio или используем svcutil.exe , такой интерфейс создается вместе с другими типами. Привлекательно й особенностью этого автоматически сгенерированного интерфейса является то, что он реализует IDisposable вместе со всеми сервисными операциями. W CF понимает этот тип, что делает реализацию IProductChannelFactory тривиальной, поскольку мы можем использовать System.ServiceModel.ChannelFactory для создания экземпляров. Как доминирующий принцип, я предпочитаю не сохраняющие состояние и крупнозернистые интерфейсы, как IProductManagementAgent , чтобы оградить пользователей от деталей реализации. Хотя мы должны рассматривать одноразовые зависимости как протекающие абстракции, утечка может содержаться в конкретной реализации; и, делая это, мы получаем тестируемость без ущерба для общей структуры. Абстрактная фабрика является чрезвычайно полезным шаблоном проектирования. Он помогает решать проблемы с зависимостями времени выполнения и недолговечными зависимостями. Мы также можем включить его в попытку решить проблемы с циклическими зависимостями, но это не играет центральную роль в данном контексте. 208 6.3. Р азрешение циклических зависимостей Иногда реализации зависимости оказывают ся циклическими. Одна реализация требует еще одну зависимость, реализация которой требует первую абстракцию. Такой граф зависимостей не может быть удовлетворительным. Важно понимать, что абстракции сами по себе могут быть совершенно нецикличн ыми, а конкретная реализация может ввести цикл. На рисунке 6-7 показано, как это может произойти. Рисунок 6-7: Циклы в графе зависимостей могут произойти даже тогда, когда абстракции не имеют отношения друг к другу. В этом примере каждая реализация реализует отдельный интерфейс, но также требует зависимость. Поскольку Concretec требует IA , но единственной реализацией IA является ConcreteA со своей зависимостью для IB и так далее, то есть у нас есть цикл, который не может быть разрешен, как есть. Пока цикл остается, мы не можем удовлетворить все зависимости, и наши приложения не будут иметь возможность запускаться. Ясно, что надо что-то делать, но что? В этом разделе мы рассмотрим вопрос о циклических зависимостях, в том числе на примере. Когда мы закончили, ваша первая реакция должна состоять в том, чтобы попытаться переделать свои зависимости. Если это невозможно, вы можете разорвать порочный круг путем рефакторинга от внедрения в конструктор ко внедрению в свойство. Это представляет собой ослабление инвариантов класса, так что это нужно делать очень обдуманно. Разрешение проблем с циклами зависимостей Всякий раз, когда я сталкиваюсь с циклом зависимости, вот мой первый вопрос: "Где я ошибся?" Совет Цикл зависимостей указывает на «плохо пахнущий» код. Если такое появится, вы должны серьезно пересмотреть структуру и код. 209 Цикл зависимостей должен немедленно вызвать тщательную оценку причин цикла. Они часто основаны либо на неправильных предположениях, либо на серьезных нарушениях правил однонаправленных зависимостей. В слоевом приложении классы должны говорить с другими классами только в своем слое или близлежащем нижнем слое. Если цикл проходит более одного слоя, мы знаем, что что-то в корне неверно. Как показано на рисунке 6-8, это обозначает, что некоторые ссылки идут не в ту сторону. Рисунок 6-8: Когда цикл пересекает одну или более границ слоя, по крайней мере, одна ссылка архитектурно незаконна. В данном случае, ссылка от D до А является незаконной. Если такая ситуация возникает, решать ее нужно немедленно. Если у нас есть цикл в пределах одного слоя, то немного непонятно, что происходит. Это даже может быть результатом хорошей идеи, которая просто закончилась неудачной реализацией. Нам необходимо сломать цикл любым способом. Пока цикл существует, приложение не будет работать. Любой цикл является плохо пахнущим кодом, поэтому наша первая реакция должна заключаться в том, чтобы переделать вовлеченные части и предотвратить цикл. Таблица 6-1 показывает некоторые общие направления, в которых мы можем двигаться. Таблица 6-1: Некоторые стратегии разработки, чтобы сломать цикличные зависимости Стратегия Описание События Вы можете часто сломать цикл, изменив одну из абстракций так, чтобы она вызывала события вместо явного вызова зависимости, сообщающие зависимости, что что-то произошло. События особенно уместны, если только одна сторона вызывает void методы для своей зависимости. .NET события являются применением шаблона проектирования Наблюдатель (Observer), и вы можете иногда рассматривать вопрос о явной реализации. Это особенно верно, если вы решите использовать доменные события (Domain Event s), чтобы разорвать порочный круг. Т ут есть потенциал обеспечить возможность истинной асинхронной односторонней передачи сообщений. Внедрение в свойство Если ничего не помогает, мы можем разорвать порочный круг путем рефакторинга одного класса от внедрения в конструктор ко внедрению в свойство. Это самый крайний вариант, потому что он только лечит симптомы. 210 Я не намерен тщательно исследовать первый вариант, потому что существующая литература уже предоставляет подробную информацию. Совет Попытайтесь решить проблему циклов с помощью событий. Если это не поможет, попробуйте Наблюдатель. И только если у вас все еще не получается разорвать цикл, используйте внедрение в свойство. Не ошибитесь: цикл зависимостей является плохо пахнущим кодом. Наш первый приоритет заключается в том, чтобы проанализировать код и понять, почему появился цикл. Когда мы поймем, почему, мы должны изменить структуру и код. Тем не менее, иногда мы не можем изменить дизайн. Даже если мы понимаем причину возникновения цикла, API-«нарушитель» может быть вне нашего контроля. Преры вание цикла при помощи внедрения в свойство В некоторых случаях ошибка проектирования находится вне нашего контроля, но нам все еще нужно разорвать порочный круг. В таких случаях мы можем сломать цикл, используя внедрение в свойство. Внимание Вы должны прибегать к решению проблемы с циклами при помощи внедрения в свойство только в крайнем случае. Оно лечит симптомы, а не лечит болезнь. Чтобы разорвать порочный круг, мы должны проанализировать его, чтобы выяснить, где мы можем сделать разрыв. Поскольку использование внедрения в свойство предлагает факультативную, а не необходимую зависимость, очень важно тщательно проверить все зависимости, чтобы определить, где разрыв принесет наименьший вред. На рисунке 6-9, B требует экземпляр IC (интерфейс, который реализует C ). Мы можем разорвать цикл, изменив зависимость для B от внедрения в конструктор во внедрение в свойство. Это означает, что мы можем сначала создать B и внедрить его в A , а затем впоследствии присвоить С B : var b = new B(); var a = new A(b); b.C = new C(new D(a)); Рисунок 6-9: Учитывая цикл, мы должны сначала решить, где его оборвать. В данном случае мы решили сломать цикл между В и С 211 Использование внедрения в свойство таким способом добавляет дополнительну ю сложность в B , потому что теперь он должен быть в состоянии работать тогда, когда его зависимость пока не доступна. С овет Классы никогда не должны выполнять работу, вовлекая зависимости в свои конструкторы, потому что внедренная зависимость может быть еще не полностью инициализирована. Если мы не хотим ослабить любой оригинальный класс таким образом, мы можем ввести виртуальную прокси (Virt ual Proxy), которая оставляет B нетронутым: var lb = new LazyB(); var a = new A(lb); lb.B = new B(new C(new D(a))); LazyB реализует IB , как это делает B . Тем не менее, он принимает свою зависимость IB через внедрение в свойство вместо внедрения в конструктор, что позволяет нам разорвать цикл, не нарушая инвариантов любого из оригинальных классов. Хотя классы с образными именами A - D иллюстрируют структуру решения, более реалистичный пример является более оправданным. Пример: создание окна Одна из наиболее распространенных ситуаций, когда мы не можем перестроить наш выход из цикла зависимостей, это когда мы имеем дело с внешними API. Одним из таких примеров является WPF. В WPF мы можем использовать MVVM паттерн, чтобы реализовать разделение понятий путем деления кода на представления и лежащие в основе модели. Модели присваиваются представлению через свойство DataContext . Это, по существу, внедрение в свойство в действии. С овет Вы можете прочитать больше о создании W PF приложений при помощи MVVM в разделе 7.4. DataContext служит как зависимость для Window , но модель играет большую роль в управлении тем, какие представления и где активируются. Одним из действий, которое модель должна быть в состоянии выполнить, – это сделать всплывающим диалоговое окно. И один из способов реализации этого заключается во внедрении абстракции, как эта, в модель: public interface IWindow { void Close(); IWindow CreateChild(object viewModel); void Show(); bool? ShowDialog(); } 212 С внедренным IWindow любая модель может создать новые Window и отобразить их в виде модальных или немодальных окон. Однако чтобы реализовать этот интерфейс, мы должны иметь ссылку на реальный Window , чтобы правильно установить свойство Owner В следующем листинге показана реализация метода CreateChild Листинг 6-2: Создание дочернего окна public virtual IWindow CreateChild(object viewModel) { var cw = new ContentWindow(); cw.Owner = this.wpfWindow; cw.DataContext = viewModel; WindowAdapter.ConfigureBehavior(cw); return new WindowAdapter(cw); } ContentWindow – это WPF окно, которое вы можете использовать, чтобы показать новое окно. Важно установить владельца Window , прежде чем показыват ь его, потому что иначе могут произойти странные ошибки, когда фокусированное или модальное окно скрыто за другими окнами. Чтобы предотвратить такие ошибки, вы устанавливаете свойство Owner для текущего Window . Поле wpfWindow является другим экземпляром System.Windows.Window Вы также присваиваете viewModel новому Window DataContext , прежде чем обернуть его в новую реализацию IWindow и вернуть его. Вопрос в том, что с этой реализацией у вас есть ViewModel , которые требуют IWindow , реализация IWindow , которая требует WPF Window , и WPF Window , которые через их DataContext требуют, чтобы работала ViewModel . Рисунок 6-10 показывает этот цикл. Рисунок 6-10: Цикл WPF M VVM. В MVVM Window зависит от ViewModel , которая, в свою очередь, зависит от экземпляра IWindow . Надлежащей реализацией IWindow является WindowAdapter , который зависит от WPF Window , чтобы иметь возможность установить владельца каждого Window и избежать ошибок фокусировки. Мы ничего не можем тут изменить, чтобы выйти из циклической зависимости. Связь между Window и ViewModel зафиксирована, потому что System.Windows.Window является внешним API (определенным в BCL). Кроме того, WindowAdapter зависит от Window , чтобы избежать ошибок фокусировки, так что это отношение дано также и извне. Единственное отношение, которое можно изменить, это только между ViewModel и его IWindow . Т ехнически вы можете перепроектировать все это, чтобы использовать события, но это приведет к довольно нелогичному API. Для отображения диалогового окна вам нужно было бы вызвать событие и надеяться, что кто-то подпишется, показывая модальное окно. Кроме того, вам пришлось бы возвращать результат диалогового окна по |