Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
![]()
|
конт ейнеры вписываются в общую картину. Хотя в главе 2 содержится развернутый пример, я уверен, что после первых глав у вас остались еще некоторые нерешенные вопросы. В части 2 мы копнем немного глубже, чтобы ответить на некоторые из этих вопросов. Как следует из названия части 2, это каталог паттернов, анти-паттернов и рефакторинга. Некоторые люди не любят паттерны проектирования, потому что считают их сухими или слишком абстрактными. Лично я люблю паттерны, потому что они обеспечивают нас языком высокого уровня, который делает нас более эффективными и краткими, когда мы обсуждаем программное обеспечение. Это мое намерение – использовать данный каталог, чтобы показать язык паттернов для DI. Хотя описание паттерна должно содержать некоторые обобщения, я попытался сделать каждый шаблон конкретным, с использованием примеров. Вы можете прочитать все три главы в последовательно сти, но каждый элемент в каталог также описан так, что вы можете прочитать о нем отдельно. Глава 4 содержит мини-каталог паттернов проектирования DI. В некотором смысле, эти паттерны представляют собой нормативное руководство о том, как реализовать DI, но вы должны знать, что я не считаю, что все они имеют одинаковое значение. Constructor Injection на сегодняшний день является наиболее важным паттерном проектирования, тогда как все остальные паттерны должны рассматриваться как крайние случаи, которые могут применяться в определенных условиях. Паттерн Am bient Context, в частности, должен быть использован настолько редко, что я серьезно задумывался не включать его в книгу (я оставил его только потому, что те, кто читают книги перед публикацией, попросили меня оставить его). В то время как глава 4 дает вам набор обобщенных решений, глава 5 содержит каталог ситуаций, которых необходимо избегать. Эти анти-паттерны описывают общие, но неправильные пути решения типичных проблем DI. В каждом случае анти-паттерн рассказывает, как выявить проблему и как ее решить. Важно знать и понимать эти анти- паттерны, чтобы избегать ловушек, которые они расставляют, и поскольку в главе 4 представлен наиболее важный паттерн, наиболее важным анти-паттерном является Service Locator. Когда вы начнете применять DI в реальных задачах программирования, вы столкнетесь с некоторыми проблемами. Я думаю, у нас всех были моменты сомнений, когда мы чувствовали, что мы понимаем инструмент или технику, и все же мы думали: «В теории это может сработ ат ь, но мой случай особенный ..." Всякий раз, когда я ловлю себя на такой мысли, я понимаю, что мне многому еще предстоит научиться. Во время моей карьеры я видел определенный набор проблем, которые появляются вновь и вновь. Каждая из этих проблем имеет общее решение, которое можно применить для перемещения вашего кода к одному из паттернов DI из главы 4. В духе рефакт оринга патт ернов я решил назвать эту главу DI рефакторингом, потому что она содержит каталог проблем и соответствующих решений. 120 Часть 2 представляет полный каталог паттернов, анти-антипаттерно в и рефакторинга. Я думаю, что это будет самая полезная часть книги, потому что она наиболее «устойчивая». Будем надеяться, что вы вернетесь к этим главам через месяцы или даже годы после первого прочтения. 4. DI паттерны 5. DI анти-паттерны 6. DI рефакторинг 121 4. DI паттерны Меню Constructor Injection Propert y Injection Method Injection Am bient Context Как и все профессионалы, повара имеют свой жаргон, который позволяет им общаться о сложных процессах приготовления пищи на своем языке, что часто звучит очень непонятно для остальных из нас. Не помогает даже то, что по большей части они используют термины, основанные на французском языке (даже если вы и говорите по- французски, такая проблема есть). Соусы являются отличным примером того, как повара используют свою профессиональну ю терминологию. В главе 1 мы кратко обсудили sauce béarnaise, но я не уточнил таксономию, которая его окружает (см. рисунок 4-1). Рисунок 4-1: Некоторые соусы основаны на sauce hollandaise. В sauce béarnaise лимон заменяется уменьшенным количеством уксуса и определенными травами, в то время как отличительной чертой sauce mousseline является то, что в него вымеш ивают ся взбитые сливки: эта технология также используется для изготовления m ousse au chocolat. sauce béarnaise – это в действительности sauce hollandaise, где лимонный сок заменяется уменьшенным количеством уксуса, луком-шалото м, кервелем и эстрагоном. Другие соусы основаны на sauce hollandaise, в том числе мой любимый, sauce m ousseline, который сделан с вымешиванием взбитых сливок в hollandaise. Вы заметили жаргон? Вместо того чтобы сказать "тщательно вбивая сливки в соус, старайтесь не свернуть его," я использовал термин вымешайт е. Когда вы знаете, что это значит, это намного легче сказать и понять. Термин вымешат ь не ограничивается соусами: это общий способ совместить что-то взбитое с другими ингредиентами. При создании классического шоколадного мусса (mousse au chocolat), например, я вымешиваю взбитые яичные белки в смесь взбитых яичных желтков и растопленного шоколада. При разработке программного обеспечения у нас самих есть сложный и непроходимый жаргон. Хотя вы можете и не знать, что относится к поварскому термину bain-m arie, я 122 уверен, что большинство поваров будут совершенно потеряны, если вы скажете им, что "строки являются неизменными классами, представляющими последовательност и Unicode символов". Когда дело доходит до разговоров о том, как структурировать код для решения конкретного типа задач, у нас есть пат терны проектирования, которые дают имена общим решениям. Т аким же образом, как термины sauce hollandaise и вымеш иват ь помогают нам поговорить о том, как сделать sauce m ousseline, паттерны помогают нам говорить о том, как структурирован код. Система событий в .NET основана на паттерне проектирования Наблюдатель (Observer), а циклы foreach на Итератор (Iterator). В этой главе я расскажу о четырех основных DI паттернах, перечисленных на рисунке 4-2. Поскольку глава построена так, чтобы показать каталог паттернов, каждый паттерн описан так, чтобы его можно было прочитать отдельно. Тем не менее, Внедрение в конструктор (Constructor Injection) на сегодняшний день является наиболее важным из четырех паттернов. Рисунок 4-2: Структура этой главы принимает форму каталога паттернов. Каждый паттерн описан так, что вы можете прочитать о нем отдельно от других паттернов. Не беспокойтесь, если вы обладаете ограниченными знаниями паттернов проектирования в целом. Основная цель паттерна проектирования заключается в создании подробного и автономного описания конкретного способа достижения цели – рецепта, если угодно. Для каждого шаблона я приведу краткое описание, пример кода, преимущества и недостатки и так далее. Вы можете прочитать обо всех четырех паттернах или ограничиться только теми, которые вас интересуют. Наиболее важным паттерном является Внедрение в конструктор (Constructor Injection), который вы должны использовать в большинстве ситуаций; вы увидите другие все более и более специализированные паттерны по мере продвижения по главе. 4.1. Внедрение в конструктор (Constructor Injection) 4.2. Внедрение в свойство (Propert y Injection) 4.3. Внедрение в метод (Method Injection) 4.4. Окружающий контекст (Am bient Cont ext) 4.5. Резюме 123 4.1. Внедрение в конструктор (Constructor Injection) Как мы можем гарантировать, чт о необходимая зависимост ь всегда дост упна для класса, кот орый мы в наст оящ ее время разрабат ываем? Запрашивая все вы зывающие элеме нты, чтобы доставить зависимость в качестве параме тра конструктору класса. Рисунок 4-3: NeedyClass нуждается в экземпляре зависимости, чтобы работать, поэтому он требует любого клиента, чтобы предоставить экземпляр через свой конструктор. Это гарантирует, что экземпляр доступен NeedyClass всякий раз, когда он необходим. Когда класс требует экземпляр зависимости, чтобы работать вообще, мы можем предоставить эту зависимость через конструктор класса, включая ее, чтобы сохранить ссылку для будущего (или немедленного) использования. Как это работает Класс, которому нужна зависимость, должен предоставить открытый конструктор, который принимает экземпляр необходимой зависимости в качестве аргумента конструктора. В большинстве случаев, это должен быть только доступный конструктор. Если необходима более чем одна зависимость, могут быть использованы дополнительные аргументы конструктора. Листинг 4-1: Внедрение в конструктор 1. private readonly DiscountRepository repository; 2. public RepositoryBasketDiscountPolicy( 3. DiscountRepository repository) 4. { 5. if (repository == null) 6. { 7. throw new ArgumentNullException("repository"); 8. } 9. this.repository = repository; 10. } 124 Строки 2-3: Внедрить зависимость как аргумент конструктора Строки 5-8: Ограждающее условие (Guard Clause) Строка 9: Сохранение зависимости для дальнейшего использования Строка 1: Поле зависимости только для чтения Зависимость (в предыду щем листинге это абстрактный класс DiscountRepository ) является требуемым аргументом конструктора. Любой клиентский код, который не предоставляет экземпляра зависимости, не может скомпилироваться. Однако так как интерфейсы и абстрактные классы являются ссылочными типами, вызывающий элемент может передать null в качестве аргумента, чтобы вызывающий код мог быть скомпилирован, и мы должны защитить класс против таких злоупотреблений при помощи ограждающего условия (Guard Clause). Поскольку объединенные усилия компилятора и ограждающего условия гарантируют, что аргумент конструктора является действительным, если не выбрасывается исключение, тогда конструктор может сохранить зависимость для будущего использования, ничего не зная о реальной реализации. Хорошей практикой является то, чтобы отметить поле, содержащее зависимость, как readonly : это гарантирует, что как только инициализационная логика конструктора будет выполнена, поле не может быть изменено. Это не является обязательным с точки зрения DI, но это защитит вас от случайного изменения поля (например, установка его на null ) где-то в другом месте зависимого от класса кода. П римечание Не загружайте конструктор любой другой логикой. Принцип единственной обязанности (Single Responsibility Principle) предполагает, что члены должны делать только одну вещь, и теперь, когда мы используем конструктор для внедрения зависимостей, мы предпочли бы держать его свободным от других вещей. П римечание Подумайте о внедрении в конструктор как о ст ат ическом объявлении зависимост ей класса. Сигнатура конструктора компилируется при помощи типа и доступна для всеобщего обозрения. Она четко говорит, что класс требует зависимости, которые он запрашивает через свой конструктор. После возвращения конструктора новый экземпляр класса находится в согласованном состоянии с соответствующим экземпляром зависимости, внедренном в него. Поскольку он содержит ссылку на эту зависимость, он может использовать ее так часто, как необходимо. Тут не нужна проверка на null , поскольку гарантировано, что экземпляр будет иметься в наличие. 125 Когда это использовать Внедрение в конструктор должно быть вашим выбором по умолчанию для DI. Оно решает наиболее распространенную задачу, где класс требует одну или несколько зависимостей и не имеется никаких приемлемых Local Default. Внедрение в конструктор хорошо решает эту задачу, потому что оно гарантирует, что зависимость присутствует. Если класс абсолютно не может функционировать без зависимости, эта гарантия является ценной. С овет Если это вообще возможно, ограничивайтесь одним конструктором. Перегруженные конструкторы приводят к неоднозначности: какой конструктор должен использовать DI контейнер? В случаях, когда локальная библиотека можем предоставить хорошую реализацию умолчанию, внедрение в свойство (Property Injection) может подойти лучше, но это нечастный случай. В предыдущих главах я показал много примеров хранилищ в качестве зависимостей. Это хорошие примеры зависимостей, когда локальная библиотека не может предоставить подходящую реализацию по умолчанию, так как надлежащая реализация принадлежит специализированным библиотекам доступа к данным (Data Access). Таблица 4-1: Преимущества и недостатки внедрения в конструктор Преимущества Недостатки Внедрение точно есть Некоторые фреймворки усложняют использование внедрения в конструктор Легко реализовать Помимо гарантированного внедрения, о чем уже говорилось, этот паттерн также легко реализовать с помощью четырех шагов, показанных в листинге 4-1. Основным недостатком внедрения в конструтор является то, что вам нужно изменить текущую платформу приложения для его поддержки. Большинство фреймворков предполагают, что у ваших классов будет конструктор по умолчанию, и что им может потребоваться специальная помощь для создания экземпляров, когда конструктор по умолчанию отсутствует. В главе 7 я объясню, как включить внедрение в конструктор для общих фреймворков. Очевидным недостатком внедрения в конструктор является то, что он требует, чтобы весь граф зависимости инициализирова лся немедленно, часто при запуске приложения. Однако, хотя это звучит не очень приятно, это редко является проблемой. В конце концов, даже для сложного графа объекта мы обычно говорим о создании десятков новых экземпляров объектов, а создание экземпляра объекта это то, что .NET Fram ework делает очень быстро. Любые узкие места, которые может иметь ваше приложение, появятся и в других местах, так что не беспокойтесь об этом. В очень редких случаях это может все же быть реальной проблемой, но в главе 8 я опишу опцию Delayed , которая является одним из возможных средств решения этой проблемы. 126 На данный момент я просто скажу, что могут (в крайнем случае) быть потенциальные проблемы с начальной загрузкой, и давайте двигаться дальше. Использование Хотя внедрение в конструктор, как правило, повсеместно в приложениях, использующих DI, оно не очень часто присутствует в .NET Base Class Library (BCL). Главным образом это потому, что BCL представляет собой набор библиотек, а не полноценное приложение. Два взаимосвязанных примера, когда мы видим своего рода внедрение конструктора в BCL, это с System.IO.StreamReader и System.IO.StreamWriter . Оба принимают в свои конструкторы экземпляр System.IO.Stream . У них также есть много перегруженных конструкторов, которые принимают путь к файлу, а не экземпляр Stream , но есть методы, которые внутренне создают FileStream на основе указанного пути к файлу: далее показаны все конструкторы StreamWriter , но конструкторы StreamReader схожи: public StreamWriter(Stream stream); public StreamWriter(string path); public StreamWriter(Stream stream, Encoding encoding); public StreamWriter(string path, bool append); public StreamWriter(Stream stream, Encoding encoding, int bufferSize); public StreamWriter(string path, bool append, Encoding encoding); public StreamWriter(string path, bool append, Encoding encoding, int bufferSize); Класс Stream является абстрактным классом, который служит в качестве абстракции, над которой работают StreamWriter и StreamReader для выполнения своих обязанностей. Вы можете указать любой реализацию Stream в их конструкторах, и они будут ее использовать, но они выбросят ArgumentNullExceptions , если вы попытаетесь присвоить Stream null Хотя BCL может предоставить нам примеры, когда мы видим, как используется внедрение конструктора, но всегда более поучительно увидеть пример. Следующий раздел проведет вас через полный пример реализации. Пример: Добавление выбора валюты для корзины покупок Я хотел бы добавить новую функцию к примеру коммерческого приложения, который я представил в главе 2, а именно, способность выполнять конвертацию валют. Я расширю пример в данной главе, чтобы продемонстрировать различные DI паттерны в деле, и когда я все сделаю главная страница должно быть как на рисунке 4-4. 127 Рисунок 4-4: Пример коммерческого приложения с реализованной конвертацией валюты. Т еперь пользователь может выбрать три разные валюта, и цена товара и общая стоимость (на странице корзины) будут отображаться в этой валюте. Одна из первых вещей, которая вам нужна, это CurrencyProvider – зависимость, предоставляющая вам валюты, которые вы запрашиваете. Вы определяете это следующим образом: public abstract class CurrencyProvider { public abstract Currency GetCurrency(string currencyCode); } Класс Currency является другим абстрактным классом, который обеспечивает конверсию между собой и другими валютами: public abstract class Currency { public abstract string Code { get; } public abstract decimal GetExchangeRateFor(string currencyCode); } Вы хотите, чтобы функция конвертации валюты была на всех страницах, где отображаются цены, так что вам нужна она в обоих HomeController и BasketController Поскольку обе реализации очень похожи, я покажу только BasketController CurrencyProvider , скорее всего, представляют собой ресурс «вне процесса», например, веб-сервис или базу данных, которые предоставляют курсы конверсии. Это означает, что наиболее подходящим была бы реализация конкретного CurrencyProvider в отдельном проекте (например, в библиотеке Data Access). Следовательно, здесь нет никакой подходящей Local Default. В то же время, классу BasketController понадобится наличие CurrencyProvider ; внедрение в конструктор – это то, что надо. Следующий листинг показывает, как зависимость CurrencyProvider внедряется в BasketController 128 Листинг 4-2: Внедрение CurrencyProvider в BasketController 1. private readonly IBasketService basketService; 2. private readonly CurrencyProvider currencyProvider; 3. public BasketController(IBasketService basketService, 4. CurrencyProvider currencyProvider) 5. { 6. if (basketService == null) 7. { 8. throw new 9. ArgumentNullException("basketService"); 10. } 11. if (currencyProvider == null) 12. { 13. throw new 14. ArgumentNullException("currencyProvider"); 15. } 16. this.basketService = basketService; 17. this.currencyProvider = currencyProvider; 18. } Строки 3-4: Внедрить зависимость как аргумент конструктора Строки 6-15: Ограждающее условие (Guard Clause) Строки 16-17: Сохранение зависимости для дальнейшего использования Строки 1-2: Поле зависимости только для чтения Поскольку класс BasketController уже имеет зависимость в IBasketService , вы добавляете новую зависимость CurrencyProvider в качестве второго аргумента конструктора, а затем следуете той же последовательно сти, что изложена в листинге 4-1: ограждающее условие гарантирует, что зависимости не являются null , что означает, что хранить их для последующего использования в readonly полях безопасно. Т еперь, когда CurrencyProvider гарантированно присутствует в BasketController , он может быть использован в любом месте, например, в методе Index : public ViewResult Index() { var currencyCode = this.CurrencyProfileService.GetCurrencyCode(); var currency = this.currencyProvider.GetCurrency(currencyCode); // … } Я еще не обсуждал CurrencyProfileService , так что теперь знайте, что он предоставляет предпочтительный код валюты текущего пользователя. Далее мы будем обсуждать CurrencyProfileService более подробно. С имеющимся кодом валюты может быть вызван CurrencyProvider , чтобы предоставить Currency , который представляет этот код. Заметьте, что вы можете использовать поле currencyProvider без необходимости проверять его заранее, потому что оно гарантированно присутствует. Т еперь, когда у вас есть Currency , вы можете продолжить, чтобы выполнить остальную работу в методе Index ; обратите внимание, что я еще не показал эту реализацию. По мере |