Главная страница

Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей


Скачать 5.66 Mb.
НазваниеРуководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
АнкорВнедрение зависимостей в .net
Дата14.12.2019
Размер5.66 Mb.
Формат файлаpdf
Имя файлаВнедрение зависимостей в .NET.pdf
ТипРуководство
#100226
страница13 из 43
1   ...   9   10   11   12   13   14   15   16   ...   43
Мэри: Нам нужен экземпляр
ProductRepository
в классе
ProductService
. Тем не менее,
ProductRepository
является абст ракт ным , поэт ому мы не можем прост о создат ь его
новые экземпляры, и наш консультант говорит , что мы не должны также создават ь
новые экземпляры
SqlProductRepository
.
Йенс: Как насчет фабрики?
Мэри: Да, я подумала о т ом же, но я не знаю, что делат ь дальш е. Я не понимаю, как она
решит наши проблемы. Смот ри ...
Мэри начинает писать код, чтобы показать проблему.
Конкретная фабрика
Это код, который пишет Мэри: public class ProductRepositoryFactory
{ public ProductRepository Create()
{ string connectionString =
ConfigurationManager.ConnectionStrings
["CommerceObjectContext"].ConnectionString; return new SqlProductRepository(connectionString);
}
}
Мэри: Эта
ProductRepositoryFactory
инкапсулирует знания о том, как создат ь
экземпляры
ProductRepository
, но это не реш ает проблемы, потому что нам нужно
использоват ь это в
ProductService
вот т ак: var factory = new ProductRepositoryFactory(); this.repository = factory.Create();
Мэри: Видишь? Теперь мы прост о должны создат ь новый экземпляр класса
ProductRepositoryFactory
в
ProductService
, но это все же жест ко кодирует

167
использование
SqlProductRepository
. Единст венное, чего мы добились, эт о переместили
проблему в другой класс.
Йенс: Да, я вижу... А мы не можем реш ит ь проблему при пом ощ и абст рактной фабрики?
Давайте поставим на паузу обсуждения Мэри и Йенса, чтобы оценить то, что произошло.
Мэри совершенно права в том, что класс конкретной фабрики не решает проблему Control
Freak, а только перемещает ее дальше. Это делает код более сложным.
ProductService теперь непосредственно контролирует жизненный цикл фабрики, а фабрика непосредственно управляет жизненным циклом
ProductRepository
, поэтому мы до сих пор не может перехватить или заменить экземпляра repository во время выполнения.
П римечание
Не делайте выводов по этому разделу, что я вообще против использования классов конкретной фабрики. Конкретная фабрика может решить другие проблемы, такие как повторение кода, путем инкапсуляции сложной логики создания. Она просто не имеет никакой ценности для DI. Используйте ее, когда это имеет смысл.
Довольно очевидно, что конкретная фабрика не решит никаких проблем DI, и я думаю, что я никогда видел, чтобы она использовалась таким образом. Комментарий Йенса об абстрактной фабрике звучит более перспективно.
Абстрактная фабрика
Давайте продолжим дискуссию Мэри и Йенса и посмотрим, что Йенс может сказать об абстрактной фабрике.
Йенс: А что если мы сделаем фабрику абст рактной? Наприм ер, вот т ак: public abstract class ProductRepositoryFactory
{ public abstract ProductRepository Create();
}
Йенс: Эт о означает, что мы жестко не кодируем любые ссылки на
SqlProductRepository
, и мы можем использоват ь фабрику в
ProductService
, чт обы
получит ь экземпляры
ProductRepository
.
Мэри: Но т еперь, когда фабрика абст ракт ная , как мы получим ее новые экземпляры?
Йенс: мы создадим ее реализацию, кот орая возвращает экземпляры
SqlProductService
.
Мэри: Да, но как мы создадим экземпляр эт ого?
Йенс: мы прост о используем ключевое слово
new
в
ProductService
... Ой, подожди...
Мэри: Это прост о вернет нас назад, от куда мы начали.
Мэри и Йенс быстро поняли, что абстрактная фабрика не меняет ситуацию. Суть заключается в том, что им нужен был экземпляр абстрактного класса
ProductRepository
, а теперь вместо этого нужен экземпляр абстрактной
ProductRepositoryFactory

168
Абстрактная фабрика
Абстрактная фабрика является одним из шаблонов проектирования из оригинальной книги Design Patterns. Она полезна для DI, потому что она может инкапсулировать сложную логику, которая создает другие зависимости.
Она предлагает хорошую альтернативу полной передачи управления, которая участвует в инверсии управления, потому что она частично позволяет потребителю контролировать жизненный цикл зависимостей, созданных фабрикой; фабрика по-прежнему управляет тем, что создается и как это создается.
Паттерн Abstract Factory встречается гораздо чаще, нежели вы можете ожидать: имена включенных классов часто скрывают этот факт. Класс
CurrencyProvider представленный в разделе 4.1.4, на самом деле является абстрактной фабрикой с другим именем: это абстрактный класс, который создает экземпляры другого абстрактного класса (
Currency
).
В разделе 6.1 мы вернемся к паттерну Abstract Factory, чтобы увидеть, как он может помочь в решении одного типа проблем, которые часто происходят с DI.
Мэри и Йенс сделали только такую реализацию фабрики, которая не была бы вредна для них. С другой стороны, это также не решит их проблемы; а поскольку предполагается, что логика создания для экземпляров
ProductRepository не будет сложной, такое использование не имеет никакой ценности.
Т еперь, когда Мэри и Йенс отвергли единственную безопасную реализацию фабрики, только один вариант остается открытым.
С татическая фабрика
Мэри и Йенс собираются прийти к выводу. Давайте послушаем, как они принимают решения о подходе, который, как они думают, будет работать:
Мэри: Давай сделаем стат ическую фабрику. Я тебе покажу: public static class ProductRepositoryFactory
{ public static ProductRepository Create()
{ string connectionString =
ConfigurationManager.ConnectionStrings
["CommerceObjectContext"].ConnectionString; return new SqlProductRepository(connectionString);
}
}
Мэри: Теперь, когда класс ст ат ический, нам не нужно думат ь, как его создат ь.
Йенс: Но у нас по-прежнему ост ает ся жесткая закодированност ь в т ом, чт о мы
возвращ аем экземпляры
SqlProductRepository
, поэт ому разве это нам как-т о
поможет?
Мэри: Мы могли бы справит ься с эт им через наст ройку конфигурации, кот орая
определяет, какой тип
ProductRepository
создат ь. Прим ерно вот так:

169 public static ProductRepository Create()
{ var repositoryType =
ConfigurationManager.AppSettings["productRepository"]; switch (repositoryType)
{ case "sql": return ProductRepositoryFactory.CreateSql(); case "azure": return ProductRepositoryFactory.CreateAzure(); default: throw new InvalidOperationException("...");
}
}
Мэри: Видиш ь? Таким образом, мы можем определит ь, должны ли мы использоват ь
реализацию на основе SQL Server или реализацию на основе Windows Azure, и нам даже не
нужно перекомпилироват ь приложение, чтобы переходит ь от одной к другой.
Йенс: Супер! Это то, чт о мы сделаем. Консульт ант должен быт ь счастлив.
Есть несколько причин, почему такая статическая фабрика не дает удовлетворительного решения первоначальной цели программировании при помощи интерфейсов. Давайте посмотрим на граф зависимостей на рисунке 5-2.
Рисунок 5-2: Граф зависимостей для предполагаемого решения: статический
ProductRepositoryFactory используется для создания экземпляро в
ProductRepository
Я не приукрашиваю
Если бы я был консультантом в этом примере, я не был бы в восторге. На самом деле, такое "решение" было предложено на проекте, с которым я был связан, и я написал 14- страничный документ, описывающий, почему это не будет работать и что делать вместо этого.
Это был довольно большой проект, который был нацелен на центральную деловую область компании Fortune 500, поэтому надлежащая модульность была важна в связи со сложностью применения. К сожалению, я стал участвовать в проекте слишком поздно, и

170 мои предложения были отклонены, потому что они несли драматические изменения в уже разработанный код.
Я переключился на другие проекты, но позже я узнал, что, хотя команде удалось сделать достаточно для выполнения контракта, проект был признан неудачным, и полетели головы.
Было бы неразумным утверждать, что проект не удался только потому, что не было использовано DI, но принятый подход был симптомом отсутствия надлежащего проектирования. Я не могу сказать, что я был удивлен, узнав, что проект так и не удался.
Все классы должны ссылаться на абстрактный класс
ProductRepository
:

ProductService
, потому что потребляет экземпляры
ProductRepository

ProductRepositoryFactory
, потому что раскрывает экземпляры
ProductRepository

AzureProductRepository и
SqlProductRepository
, потому что они реализуют
ProductRepository
ProductRepositoryFactory зависит от обоих классов
AzureProductRepository и
SqlProductRepository
. Поскольку
ProductService напрямую зависит от
ProductRepositoryFactory
, он также зависит от обеих реализаций
ProductRepository
Крах зависимости
Плохая ситуация возникает, когда абстракция
ProductRepository и потребляющий
ProductService определены в одной сборке (как это имеет место с реализациями, которые я создавал в книге до сих пор). Давайте предположим, что это сборка доменной модели. В этом случае,
ProductRepositoryFactory также должна быть в этой же сборке, иначе у нас была бы циклическая ссылка, которая не представляется возможной.
Т ем не менее, фабрика имеет ссылки на обе реализации, а у них есть ссылка на сборку доменной модели, поскольку они реализуют класс
ProductRepository
. Опять же, единственный способ избежать циклической ссылки заключается в размещении конкретных реализаций в той же сборке.
Когда
AzureProductRepository и
SqlProductRepository реализованы в сборке доменной модели, это полностью идет вразрез с принципом разделения понятий (Separation of
Concerns). Мы, по существу, останемся с монолитным приложением.
Единственный выход из этой проблемы заключает ся в определении абстракции
ProductRepository в отдельной сборке. Это может быть хорошей идеей по многим другим причинам, но этого не достаточно, чтобы сделать статическую фабрику жизнеспособным решением для DI.
Вместо слабо связанных реализаций
ProductRepository
, Мэри и Йенс в итоге получили тесно связанные модули. Хуже того, фабрика всегда тянет за собой все реализации, даже те, которые не нужны.
Если Мэри и Йенсу когда-нибудь понадобится третий тип
ProductRepository
, им придется изменить фабрику и перекомпилировать решение. Хотя их решение может быть настраиваемым, оно не является расширяемым.

171
Т акже невозможно заменить конкретные реализации
ProductRepository тестовыми реализациями, такими как динамические m ock, потому что это потребует определить экземпляр ы
ProductRepository во время выполнения, а не статически в файле конфигурации во время разработки.
П римечание
Динамические mock выходят за рамки этой книги, но я кратко коснулся данной темы, когда я описывал тестируемость в главе 1 (раздел 1.2.2).
Короче говоря, кажется, что статическая фабрика решает проблему, но в действительности лишь усугубляет ее. Даже в лучшем случае это заставит вас ссылаться на непостоянные зависимости.
Т еперь, когда вы увидели много примеров Cont rol Freak, я надеюсь, у вас есть довольно хорошее представление о том, что искать: вхождения ключевого слова new рядом с зависимостями. Это может позволить вам избежать наиболее очевидных ловушек, но если вам нужно оградить себя от возникновения этого анти-паттерна, в следующем разделе обсуждается, как бороться с такой проблемой.
Анализ
Control Freak является антитезой инверсии управления. Когда мы напрямую управляем созданием изменчивых зависимостей, мы в конечном итоге получаем тесно связанный код, теряя многие (если не все) преимущества слабой связанности, описанной в главе 1.
Влияние
С тесно связанным кодом, который является результатом Control Freak, теряются многие преимущества модульной конструкции:

Хотя мы можем настроить приложение для работы с одной из нескольких предварительно конфигурированных зависимостей, мы не можем заменить их, когда захотим. Не возможно предоставить реализацию, которая была создана после того, как было скомпилировано приложение, и, конечно же, не возможно предоставить конкретные экземпляр ы реализации.

Все сложнее становится повторно использовать модуль, поскольку он приносит с собой зависимости, которые могут быть нежелательны в новом контексте.

Это усложняет параллельну ю разработку, потому что потребляющее приложение- тесно связано со всеми реализациями его зависимостей.

Страдает тестируемость, потому что динамические mock не могут быть использованы в качестве заменителей зависимостей.
При тщательном проектировании мы можем все еще быть в состоянии реализовывать тесно связанные приложения с четко определенными обязанностями, так что поддерживаемость не страдает, но даже при этом цена слишком высока. Нам нужно отойти от Control Freak к надлежащим DI.
Ре факторинг к DI
Чтобы избавиться от Cont rol Freak, мы должны сделать рефакторинг нашего кода к одному из паттернов проектирования DI, представленных в главе 4. В качестве первого

172 шага, мы должны использовать руководство, чтобы определить, какой паттерн выбрать. В большинстве случаев это будет внедрение в конструктор. Шаги рефакторинга заключаются в следующем:
1. Убедитесь, что вы програм мируете, используя инт ерфейсы. В примерах, которые я только что представил, это уже имело место, но в других ситуациях, возможно, потребуется сначала извлечь интерфейс и изменить объявления переменных.
2. Если вы создаете конкретную реализацию зависимостей в нескольких местах, переместите их все в один метода создания. Убедитесь, что возвращаемо е значение этого метода выражается в виде абстракции, а не конкретного типа.
3. Т еперь, когда у вас есть только одно место, где вы создаете экземпляр, переместите это создание из потребляющего класса путем реализации одного из DI паттернов, такого как внедрение в конструктор.
В случае с примерами
ProductService из предыдущих разделов, внедрение в конструктор является отличным решением: private readonly ProductRepository repository; public ProductService(ProductRepository repository)
{ if (repository == null)
{ throw new ArgumentNullException("repository");
} this.repository = repository;
}
В некоторых случаях исходный код использует сложную логику для определения того, как создавать экземпляр ы зависимости. В таком случае эта сложная логика может быть реализована в фабрике, а затем можно извлечь интерфейс такой фабрики для создания абстрактной фабрики. В сущности, это означает, что зависимость меняется, так что новая абстрактная фабрика становится зависимостью вместо первоначальной абстракции, и мы можем применить ту же логику рефакторинга к фабрике. В большинстве случаев мы в конечном итоге внедряем фабрику в потребляющий класс через его конструктор.
Control Freak является наиболее распространенным DI анти-паттерном. Он представляет собой способ по умолчанию создания экземпляров в большинстве языков программирования, так что его можно видеть даже в тех приложениях, где разработчики никогда не использовали DI. Это такой естественный и глубоко укорененный способ создания новых объектов, что многим разработчикам трудно от этого отказаться. Даже тогда, когда разработчики начинают думать о DI, многие все же полагают, что они должны как-то контролировать, когда и где создаются экземпляр ы. Отказаться от такого контроля может быть трудным психическим прыжком, но даже если вы сделаете это, есть и другие, хотя и более мелкие, ловушки, которых нужно избегать.
Control Freak на сегодняшний день является самым разрушительным анти-паттерном, но даже если у вас все под контролем, могут возникнуть более тонкие проблемы. В следующих разделах рассматриваются эти анти-паттерны. Хотя они менее проблематичны, чем Control Freak, их также, как правило, легче решить, так что внимательно смотрите и исправляйте их, когда вы их обнаружите.

173 5.2. Bastard Injection
Перегруженные конструкторы являются довольно распространенными во многих базах кода .NET (включая BCL). Часто многие перегруженные варианты обеспечивают разумные значения по умолчанию для одного или двух полномасштабных конструкторов, которые принимают все соответствующие параметры в качестве входных данных.
Время от времени мы видим другое использование, когда речь идет о DI. Довольно общий анти-паттерн определяет предназначенный для тестирования перегруженный вариант конструктора, который позволяет нам явно определить зависимость, в то время как конструктор по умолчанию используется в производственном коде.
Это может быть вредным, когда реализация зависимости по умолчанию представляет
Foreign Default, а не Local Default.
Когда мы полностью охватываем DI, такие перегруженные конструкторы становятся лишними в лучшем случае. Учитывая негативные последствия, лучше избегать их.
ProductService с Foreign Default
Когда Мэри первоначально реализовала класс
ProductService
(в главе 2), она имела ввиду только одну зависимость: реализацию на основе SQL Server. Класс
SqlProductRepository изначально был задуман как единственная реализация
ProductRepository
, поэтому казалось очевидным использовать его по умолчанию.
Foreign Default
Foreign Default является противоположност ью Local Default. Это реализация зависимости, которая используется по умолчанию, даже если она определена в другом модуле, чем ее потребитель.
В качестве примера давайте рассмотрим реализации
Repository
, которые мы видели в примере коммерческого приложения в предыду щих главах. Сервис, такой как
ProductService
, требует для работы экземпляр
ProductRepository
. Во многих случаях, когда мы разрабатываем такие приложения, мы имеем в виду разумную реализацию: ту, которая реализует нужный функционал путем чтения и записи данных в и из реляционной базы данных. Было бы заманчиво использовать такую реализацию по умолчанию.
Проблема заключается в том, что реализация по умолчанию, которую мы имеем в виду
(
SqlProductRepository
), определена в другом модуле, нежели
ProductService
. Это заставляет нас принять нежелательну ю зависимость для модуля
CommerceSqlDataAccess
, как показано здесь.
Когда
ProductService использует
SqlProductRepository в качестве реализации по умолчанию, это заставляет нас делать жесткую ссылку на модуль
CommerceSqlDataAccess
, а мы этого не хотим.

174
Использование нежелательных модулей отнимает у нас многие преимущества слабой связанности, которые обсуждались в главе 1. Все сложнее становится повторное использование модуля
CommerceDomain
, потому что он потянет за собой модуль
CommerceSqlDataAccess
, а мы, возможно, не захотим использовать это в другом контексте. Это также усложняет параллельну ю разработку, потому что класс
ProductService теперь напрямую зависит от класса
SqlProductRepository
Таковы основные причины того, что вы должны избегать Foreign Default, если это вообще возможно.
Мэри еще не чувствует себя комфортно с идеей внедрения в конструктор, потому что у нее есть проблема в выяснении, где будет размещена структура объекта. Она до сих пор не обращала внимания на концепцию Com position Root.
Приглашенный консультант сказал ей использовать внедрение в конструктор для
ProductService
, но она по-прежнему считает, что она должна создать его новый экземпляр следующим образом: var productService = new ProductService();
Для этого она добавляет следующий код в класс
ProductService
Листинг 5-2:
ProductService с Bastard Injection private readonly ProductRepository repository; public ProductService()
: this(ProductService.CreateDefaultRepository())
{
} public ProductService(ProductRepository repository)
{ if (repository == null)
{ throw new ArgumentNullException("repository");
} this.repository = repository;
} private static ProductRepository CreateDefaultRepository()
{ string connectionString =
ConfigurationManager.ConnectionStrings
["CommerceObjectContext"].ConnectionString; return new SqlProductRepository(connectionString);
}
Строки 2-5: Конструктор по умолчанию
Строки 6-13: Внедрение в конструктор

175
Класс
ProductService теперь имеет конструктор по умолчанию, который вызывает его другой конструктор, используя Foreign Default.
Другой конструктор правильно реализует паттерн внедрение в конструктор, имея ограждающее условие, а затем сохраняя внедренный
ProductRepository в поле только для чтения. Конструктор по умолчанию вызывает этот конструктор с Foreign Default, созданной в закрытом методе
CreateDefaultRepository
. Класс
SqlProductRepository является Foreign Default, поскольку он определен в другой сборке, чем класс
ProductService
. Это приводит к тому, что сборка, содержащая класс
ProductService
, тесно связана со сборкой, содержащей класс
SqlProductRepository
Хотя
ProductService можно использовать с различными типами
ProductRepository
, передавая их с помощью самого гибкого перегруженного варианта конструктора, Мэри не сможет перехватить экземпляр
ProductRepository в приложении, если она настаивает на использовании конструктора по умолчанию.
Анализ
Bastard Injection наиболее часто встречается, когда разработчики пытаются сделать свои классы тестируемыми без полного понимания DI. При написании модульных тестов для класса очень важно, чтобы мы могли заменить изменчивую зависимость дублирующим тестом, так чтобы мы могли правильно изолировать тестируемую систему (SUT) от ее зависимостей, и внедрение в конструктор позволяет сделать именно это.
Хотя Bastard Injection включает тестируемость, он имеет некоторые нежелательные последствия.
Конкретный пример: ASP.NET MVC
Когда вы создаете новый ASP.NET MVC проект, автоматически создается несколько стандартных классов контроллеров. Одним из них является класс
AccountController
, который использует Bastard Injection. В исходном коде это объясняется даже в комментариях:
// Этот конструктор используется MVC фреймворк,
// чтобы создать экземпляр контроллера при помощи
// форм аутентификации по умолчанию и провайдеров членства. public AccountController()
: this(null, null)
{
}
// Этот конструктор не используется MVC фреймворком,
// но он используется для упрощения юнит тестирования этого типа.
// Посмотрите комментарии в конце этого файла
// для более полной информации. public AccountController(IFormsAuthentication formsAuth,
IMembershipService service)
{ this.FormsAuth = formsAuth ?? new FormsAuthenticationService(); this.MembershipService = service ?? new AccountMembershipService();
}
Как я могу сказать, что Bastard Injection – это плохо, когда кажется, что Microsoft использует и одобряет его? В данном случае, мотивация, кажется, исключительно связана

176 с тестируемостью, и Bastard Injection вполне соответствует этой цели – он просто не соответствует другим целям модульности, таким как способность к замещению и повторному использованию модулей, а также параллельной разработке.
Другие придерживаются того же мнения. Айенде Райен отметил следующее в своем блоге, в котором обсуждалось ASP.NET MVC приложение:
Я имею в виду, если вы хот ит е сделат ь слабый IoC – вперед. Но, пожалуйст а, не
создавайт е этого бастарда.
Эта фраза вдохновила меня назвать анти-паттерн так, как я это сделал.
Влияние
Основная проблема с Bastard Injection заключается в использовании Foreign Default. Хотя тестируемость и включена, мы больше не можем свободно повторно использовать класс, потому что это потянет за собой зависимость, которую мы не хотим. Кроме того, параллельная разработка усложняется, потому что класс сильно зависит от своей зависимости.
В дополнение к последствиям Bastard Injection для модульности приложения, существование множества конструкторов также представляет собой другой тип проблемы.
Когда есть только один конструктор, DI контейнер может автоматически проводить все зависимости, потому что никогда не встает вопрос, какой конструктор использовать.
Когда существует более чем один конструктор, выбор между конструкторами становится неоднозначным. DI контейнеру приходится прибегать к своего рода эвристике, чтобы сделать выбор между различными конструкторами, или он может от этого отказаться. Как ни странно, это один из немногих сценариев, где бедное DI менее подвержено влиянию, потому что мы можем решать в каждом конкретном случае индивидуально, когда мы вручную проводим зависимости.
Среди различных анти-паттернов DI, Bastard Injection не так вреден, как Control Freak, но и от него также гораздо легче избавиться.
Ре факторинг по направлению к DI
Bastard Injection часто является результатом ошибочной попытки реализовать DI. Хорошо, что есть такие основы, как программирование к интерфейсам, так что провести рефакторинг к надлежащему DI паттерну легко.
С овет
Даже если вы думаете, что воздействие Bastard Injection вас не касается, вы все равно должны провести рефакторинг к надлежащему DI паттерну. Ведь это так просто сделать, что не стоит даже сомневаться в нужности этого.
Первый шаг заключается в выборе того, какой DI паттерн соответствует цели. Рисунок 5-3 иллюстрирует простой процесс принятия решений. Если значение по умолчанию, которые было использовано до сих пор, это Foreign Default, лучшим выбором является внедрение в конструктор. В другом случае, хорошей альтернативой считается внедрение в свойство.

177
Рисунок 5-3: При рефакторинге от Bastard Injection решающий фактор заключается в том, является ли зависимость Foreign Default или Local Default.
Во многих случаях, значение по умолчанию, используемое конструктором по умолчанию, представляет Foreign Default. В этих случаях внедрение в конструктор является лучшим ответом, потому что он так прост в реализации и хорошо справляется с любыми видами зависимостей. Конструктор, который принимает зависимость в качестве параметра, уже существует, так что единственное изменение, которое мы должны сделать в используемом классе, заключается в удалении конструктора по умолчанию.
Это, несомненно, приведет к некоторым ошибкам компилятора, но на данный момент мы можем опереться на компилятор и переместить весь код, который создает рассматриваемый класс, в Composition Root.
Когда значение по умолчанию представляет Local Default, ситуация очень напоминает основной сценарий для внедрения в свойство. Хотя механика и разная, структура такая же: в обоих случаях у нас есть подходящая Local Default, но мы все еще хотим открыть наш потребляющий класс для расширяемости.
Это вырождающийся случай Bastard Injection, где воздействие гораздо менее серьезное.
Поскольку значение по умолчанию является Local Default, нет никакого влияния на степень компонуемости класса; единственным негативным последствием является то, что двусмысленно сть конструктора делает автовнедрение более сложным.
В данном случае мы могли бы реализовать внедрение в свойство, удалив конструктор, который принимает зависимость в качестве параметра, и заменив его свойством для записи. Если это изменение приводит к ошибкам компилятора, мы снова можем опереться на компилятор и переместить создание кода в Composition Root .
В Com position Root есть много различных способов компоновать зависимости, в том числе такие, которые далеки от идеальных, как покажет следующий анти-паттерн.

178 5.3. Constrained Construction
Самая большая проблема должным образом реализовать DI заключается в том, чтобы все классы с зависимостями были перемещены в Composition Root. Пока мы достигнем этого, мы пройдем долгий путь.
И даже тогда все еще существуют некоторые ловушки, которые нужно отслеживать.
Распространенной ошибкой является требование того, чтобы у всех зависимостей был конструктор с конкретной сигнатурой. Как правило, это происходит от желания достичь поздней связанности, так чтобы зависимости могли быть определены во внешнем файле конфигурации и тем самым изменены без повторной компиляции приложения.
П римечание
Т ак называемый паттерн Провайдер (Provider), используемый в ASP.NET, является примером Constrained Construction, потому что Провайдеры должны иметь конструкторы по умолчанию. Это, как правило, усугубляется тем, что конструктор Провайдера пытается читать файл конфигурации приложения. Часто конструктор генерирует исключение, если необходимый раздел файла конфигурации недоступен.
П римечание
Этот раздел относится только к тем сценариям, где нужно позднее связывание. В сценариях, где мы напрямую ссылаемся на зависимости из корня приложения, у нас нет этой проблемы – но опять же, у нас также нет возможности для замены зависимостей без повторной компиляции.
В главе 3 мы кратко коснулись этого вопроса. В этом разделе мы рассмотрим его более тщательно.
Пример: отложенная связанность ProductRepository
В примере коммерческого приложения некоторые классы зависят от абстрактного класса
ProductRepository
. Это означает, что для создания этих классов в первую очередь необходимо создать экземпляр
ProductRepository
. На данный момент вы узнали, что
Composition Root – это нужное место, чтобы сделать это. В приложении ASP.NET для этого есть
Global.asax
; следующий листинг показывает соответствующую часть, где создается экземпляр
ProductRepository
Листинг 5-3: Неявное ограничение конструктора
ProductRepository
1.
string connectionString =
2.
ConfigurationManager.ConnectionStrings
3.
["CommerceObjectContext"].ConnectionString;
4.
string productRepositoryTypeName =
5.
ConfigurationManager.AppSettings
6.
["ProductRepositoryType"];
7.
var productRepositoryType =
8.
Type.GetType(productRepositoryTypeName, true);
9.
var repository =
10.
(ProductRepository)Activator.CreateInstance(
11.
productRepositoryType, connectionString);

179
Строки 9-11: Создание экземпляра конкретного типа
Первое, что должно вызвать подозрение – это то, что строка соединения считывается из файла web.config
. Зачем вам нужна строка соединения, если вы планируете обрабатывать
ProductRepository как абстракцию? Хотя, возможно, это и маловероятно, но вы можете захотеть реализовать
ProductRepository с базой данных в памяти или XML файлом.
REST -сервис хранения данных, такой как W indows Azure Table Storage Service предлагает более реалистичную альтернативу, но в очередной раз самым популярным выбором, кажется, остаются реляционные базы данных. Повсеместное распространение баз данных ведет к тому, что слишком легко забыть, что строка соединения неявно представляет выбор реализации.
Чтобы сделать позднюю привязку
ProductRepository
, вы должны определить, какой тип был выбран в качестве реализации. Это можно сделать, прочитав имя типа, определенное сборкой, из web.config и создав экземпляр типа с таким именем. Это само по себе не является проблемой – трудность возникает только тогда, когда вам нужно создать экземпляр этого типа.
С наличием
Type вы можете создать экземпляр с помощью класса
Activator
. Метод
CreateInstance вызывает конструктор типа, поэтому вы должны передать верные параметры конструктору, чтобы предотвратить исключение. В этом случае нужно указать строку соединения.
Если бы вы ничего не знали о приложении, только видели код в листинге 5-3, то вы бы удивились, почему строка соединения передается в качестве аргумента конструктора неизвестному типу. Это не имело бы больше смысла, если бы реализация была основана на REST веб-сервисе или XML файле.
На самом деле, это не имеет смысла, так как это представляет собой случайное ограничение для конструктора зависимости. В данном случае у вас есть неявное требование, чтобы любая реализация
ProductRepository имела конструктор, который принимает одну строку в качестве входных данных. Это дополнение к явному ограничению, что класс должен наследоват ься от
ProductRepository
П римечание
Неявное ограничение, что конструктор должен принять одну строку, по-прежнему оставляет нам большую степень гибкости, потому что мы можем кодировать много разной информации в строках для дальнейшего декодирования. Представьте себе, что вместо этого ограничения был бы конструктор, который принимает
TimeSpan и число, и вы можете себе представить, насколько ограничивающим это было бы.
Можно утверждать, что
ProductRepository на основе XML файла также потребует строку в качестве параметра конструктора, хотя этой строкой будет имя файла, а не строка соединения. Тем не менее, концептуально это все равно будет странно, потому что вам все равно нужно было бы определить это имя файла в элементе connectionStrings в web.config
(и в любом случае, я думаю, что такой гипотетический
XmlProductRepository должен принять
XmlReader в качестве аргумента конструктора, а не имя файла).

180
Моделирование конструкции зависимости исключительно на явных ограничениях
(интерфейса или базового класса) является намного более хорошим и более гибким вариантом.
Анализ
В предыдущем примере неявное ограничение требует от реализующих элементов иметь конструктор с одним строковым параметром. Более распространенным ограничением является то, что все реализации должны иметь конструктор по умолчанию, так чтобы работала простейшая форма
Activator.CreateInstance
: var dep = (ISomeDependency)Activator.CreateInstance(type);
Хотя это можно назвать наименьшим общим знаменателем, цена гибкости слишком высока.
Влияние
Независимо от того, как мы ограничиваем строение объекта, мы теряем гибкость. Может возникнуть соблазн заявить, что все реализации зависимостей должны иметь конструктор по умолчанию – в конце концов, они могли бы выполнять свою инициализацию внутренне, например, чтением конфигурационных данных, таких как конфигурационные строки, непосредственно из файла
.config
. Однако это ограничило бы нас по-другому, потому что мы, возможно, захотели бы иметь возможность компоновать приложение слоями экземпляров, которые включают другие экземпляры. В некоторых случаях, например, мы могли бы захотеть распределить экземпляры между различными потребителями, как показано на рисунке 5-4.
Рисунок 5-4: В этом примере мы хотим создать единственный экземпляр класса
ObjectContext и внедрить этот же экземпляр в оба репозитория. Это возможно только в том случае, если мы можем внедрить экземпляр извне.
Когда у нас есть более чем один класс, требующий одну и ту же зависимость, мы, возможно, захотим поделиться одним экземпляро м со всеми этими классами. Это возможно только тогда, когда мы можем внедрить этот экземпляр извне. Хотя мы могли бы написать код внутри каждого из этих классов, чтобы прочитать информацию о типах из конфигурационного файла, и использовать
Activator.CreateInstance для создания правильного типа экземпляра, мы никогда не смогли бы поделиться одним экземпляром таким способом – вместо этого у нас было бы несколько экземпляров одного класса, которые занимали бы больше памяти.

181
Примечание
Только потому, что DI позволяет нам делиться одним экземпляром среди многих потребителей, не означает, что мы всегда должны это сделать. Совместное использование экземпляра экономит память, но может создать проблемы взаимодействия, например, проблем многопоточности. Решение о том, хотим мы поделиться экземпляром или нет, тесно связано с концепцией жизненного цикла объекта, которая обсуждается в главе 8.
Вместо введения неявных ограничений о том, как объекты должны быть построены, мы должны реализовать наш Composition Root, так чтобы он мог работать с любым конструктором или методом фабрики, которые мы можем ему дать.
Рефакторинг по направлению к DI
Что делать, если у нас нет ограничений для компонентов конструктора, а нам нужно позднее связывание? Может возникнуть соблазн ввести абстрактную фабрику, которую можно использовать для создания экземпляров необходимой абстракции, а затем потребовать, чтобы реализации такой абстрактной фабрики имели конструкторы по умолчанию, но это приведет к тому, что мы будем лишь ходить вокруг да около проблемы, а не решать ее.
Внимание
Хотя мы можем использовать абстрактные фабрики, чтобы успешно реализовать позднее связывание, это требует дисциплины. В общем, нам будет лучше с надлежащим DI контейнером; но я, тем не менее, покажу, как это сделать.
Давайте кратко рассмотрим такой подход. Представьте себе, что у вас есть абстракция сервиса, образно называемая
ISomeService
. Схема абстрактной фабрики подсказывает, что вам также нужен интерфейс
ISomeServiceFactory
. Рисунок 5-5 иллюстрирует эту структуру.
Рисунок 5-5:
ISomeService представляет реальную зависимость. Однако чтобы сохранить реализующие элементы свободными от неявных ограничений, вы пытаетесь разрешить вопрос поздней связанности путем введения
ISomeServiceFactory
, которая будет использоваться для создания экземпляров
ISomeService
. И вам потребуется любая фабрика, поскольку у нее есть конструктор по умолчанию.
Теперь давайте предположим, что вы хотите использовать реализацию
ISomeService
, которому требуется рабочий экземпляр
ISomeRepository
, как показано в следующем листинге.

182
Листинг 5-4:
SomeService который требует
ISomeRepository public class SomeService : ISomeService
{ public SomeService(ISomeRepository repository)
{
}
}
Класс
SomeService реализует интерфейс
ISomeService
, но требует экземпляр
ISomeRepository
. Поскольку единственный конструктор не является конструктором по умолчанию, пригодится
ISomeServiceFactory
Теперь вы хотите использовать реализацию
ISomeRepository
, основанную на Entity
Framework. Вы называете эту реализацию
SomeEntityRepository
, и она определена в другой сборке, чем
SomeService
Поскольку вы не хотите перетащить ссылку в библиотеку
EntityDataAccess наряду с
SomeService
, единственным решением является реализация
SomeServiceFactory в другой сборке, чем
SomeService
, как показано на рисунке 5-6.
Рисунок 5-6: Класс
SomeServiceFactory должен быть реализован в отдельной сборке, нежели
SomeService
, чтобы предотвратить связанность библиотеки
DomainModel и библиотеки
EntityDataAccess
Хотя
ISomeService и
ISomeServiceFactory похожи на сплоченную пару, важно реализовать их в двух различных сборках, так как фабрика должна иметь ссылки на все зависимости, чтобы иметь возможность их правильно связывать.
По соглашению реализация
ISomeServiceFactory имеет конструктор по умолчанию, так что вы можете написать имя типа, определенное сборкой, в файле
.config и использовать
Activator.CreateInstance для создания экземпляра. Каждый раз, когда вам нужно связывать вместе новую комбинацию зависимостей, необходимо реализовать новую
ISomeServiceFactory
, чтобы провести именно эту комбинацию, а затем настроить приложение для использования этой фабрики вместо предыдущей. Это означает, что вы

183 не можете определить произвольные комбинации зависимостей без написания и компиляции кода, но вы можете сделать это без перекомпиляции самого приложения.
По сути, такая абстрактная фабрика становится абстрактным Composition Root , который определен в сборке отдельно от основного приложения. Хотя это, безусловно, является жизнеспособным подходом, как правило, гораздо легче использовать DI контейнер общего назначения, который может сделать все это для нас сам на основе файлов конфигурации.
Анти-паттерн Constrained Construction применяется действительно только тогда, когда мы используем позднее связывание, потому что когда мы используем раннее связывание, компилятор гарантирует, что мы никогда не введем неявные ограничения в том, как строятся компоненты.
Последний анти-паттерн применяется более часто – некоторые люди даже считают его настоящим паттерном, а не анти-паттерном.

184 5.4. Service Locator
Это может быть трудным – отказаться от идеи прямого контроля зависимостей, поэтому многие разработчики выводят статические фабрики (как описано в разделе 5.1.2) на новый уровень. Это приводит к анти-паттерну Service Locator.
Внимание
Это довольно спорно – называть Service Locator анти-паттерном. Некоторые люди считают его настоящим паттерном проектирования, тогда как другие (и я в том числе) считают анти-паттерном. В этой книге я решил описать его как анти-паттерн, потому что я думаю, что у него больше недостатков, чем преимуществ, но не удивляйтесь, если вы увидите, что он одобрен в других местах. Важно понять преимущества и недостатки, чтобы иметь возможность принять для себя обоснованное решение.
Service Locator был введен в качестве паттерна проектирования Мартином Фаулером в
2004, поэтому обозначение его как анти-паттерн является большим шагом. Короче говоря, он вводит статическую фабрику с дополнительно й деталью, что становится возможно внедрить сервисы в эту фабрику.
Примечание
Термин сервис в данном контексте приблизительно эквивалент ен термину зависимость.
Как это чаще всего реализуется, Service Locator является статической фабрикой, которая может быть сконфигурирована с конкретными сервисами, пока первый потребитель не начинает ее использовать (см. рисунок 5-7). Это, вероятно, может произойти в
Com position Root . В зависимости от конкретной реализации, Service Locator может быть настроен с кодом, когда читаются файлы конфигурации или используется их комбинации.
Рисунок 5-7: Основная ответственность Service Locator заключается в том, чтобы обрабатывать экземпляр ы сервисов, когда потребители их запрашивают.
Consumer использует интерфейс
IService и запрашивает экземпляр от Service Locator, который затем возвращает экземпляр той конкретной реализации, которую он должен вернуть
Основная обязанность Service Locator заключается в том, чтобы обрабатывать экземпляры сервисов, когда потребители их запрашивают. Потребитель использует интерфейс
IService и запрашивает экземпляр у Service Locator, который затем возвращает экземпляр любой конкретной реализации, которую он сконфигурирован вернуть.

185
Моя личная история с Service Locator
У меня с Service Locator были интенсивные отношения в течение нескольких лет, прежде чем мы расстались. Хотя я точно не помню, когда я впервые наткнулся на статью
Фаулера, мне показалось, что он предоставил мне потенциальное решение проблемы, которую я обдумывал в течение некоторого времени: как внедрить зависимости.
Как было описано, паттерн Service Locator казался ответом на все мои вопросы, и я быстро начал разрабатывать Service Locator для первой версии Microsoft patterns &
practices’ Enterprise Library. Это было размещено на ныне не существующем сайте
GotDotNet. Хотя у меня все еще есть исходный код, я потерял историю релизов, когда
GotDotNet закрыли, поэтому я не могу сказать наверняка, но я, кажется, опубликовал первую версию в середине 2005 года.
В 2007 году я выпустил полностью переписанный релиз, ориентированный на Ent erprise
Library 2. Он по-прежнему доступен на CodePlex, но я давно уже отказался от него, потому что я вскоре пришел к выводу, что это был действительно анти-паттерн.
Как вы можете видеть, у меня ушло несколько лет интенсивного использования, чтобы понять недостатки Service Locator и что существуют лучшие альтернативы. По этой причине я с легкостью понимаю, почему так много разработчиков находят его привлекательным, несмотря на его недостатки. Паттерны, описанные в главе 4, предлагают более совершенные варианты, но вы не поймете недостатки Service Locator, пока не изучите их.
Внимание
Если вы посмотрите только на статическую структуру классов, DI контейнер выглядит как
Service Locator. Разница мизерная и заключается не в механике реализации, а в том, как вы ее используете. В сущности, просьба к контейнеру или локатору разрешить полный граф зависимости из Composition Root является правильным использованием. Просьба о "зернистых" сервисах из любого другого места подразумевает анти-паттерн Service
Locator.
Давайте рассмотрим пример, где он сконфигурирован с кодом.
Пример: ProductService, использующий Service Locator
Для примера давайте вернемся к нашему проверенному и истинному
ProductService
, который требует экземпляр абстрактного класса
ProductRepository
. В данном случае
ProductService может использовать статический метод
GetService
, чтобы получить необходимый экземпляр: this.repository = Locator.GetService
();
В данном примере я реализую методы, используя параметры дженерик типа, чтобы указать тип запрашиваемого сервиса, но я также мог бы использовать экземпляр
Type
, чтобы указать тип.
Как показано в следующем листинге, эта реализация класса
Locator является минималистичной, насколько это возможно. Я мог бы добавить ограждающее условие и обработку ошибок, но я хотел выделить основное поведение. Код может также включать в

186 себя возможность, которая позволяет загружать его конфигурацию из файла
.config
, но я оставлю это в качестве упражнения для вас.
Листинг 5-5: Минимальная реализация Service Locator
1.
public static class Locator
2.
{
3.
private readonly static Dictionary services
4.
= new Dictionary();
5.
public static T GetService()
6.
{
7.
return (T)Locator.services[typeof(T)];
8.
}
9.
public static void Register(T service)
10.
{
11.
Locator.services[typeof(T)] = service;
12.
}
13.
public static void Reset()
14.
{
15.
Locator.services.Clear();
16.
}
17.
}
Строки 5-8: Получить сервис
Locator
– это класс с только статическими членами, так что вы могли бы также явно отметить его как статический класс. Он содержит все настроенные сервисы во внутреннем словаре, который связывает абстрактные типы с конкретными экземплярами.
Клиенты, такие как
ProductService
, могут использовать метод
GetService
, чтобы запросить экземпляр абстрактного типа
T
. Поскольку в этом примере кода не содержится ограждающее условие или обработка ошибок, данный метод сгенерирует довольно туманное
KeyNotFoundException
, если запрашиваемый тип не имеет записи в словаре, но вы можете представить, как добавить код, чтобы выбросить более осмысленное исключение.
Метод
GetService может возвращать только экземпляр запрошенного типа, если он ранее был вставлен во внутренний словарь. Это можно сделать с помощью метода
Register
Опять же, этот пример кода не содержит ограждающего условия, так что можно было бы зарегистрировать null
, но более надежная реализация не должна этого допустить.
В некоторых случаях (особенно при модульном тестировании), важно иметь возможность сбросить Service Locator. Эта функционально сть обеспечивается методом
Reset
, который очищает внутренний словарь.
Т акие классы, как
ProductService полагаются на сервис, чтобы быть доступными в
Service Locator, поэтому очень важно, что он был ранее настроен. В модульных тестах это может быть сделано при помощи тестового дублера (поддельного объекта, Test Double), реализованного динамической m ock библиотекой, такой как Moq, как в этом примере: var stub = new Mock
().Object;
Locator.Register
(stub);
Сначала мы создаем заглушку абстрактного класса
ProductRepository
, а затем с помощью статического метода
Register настраиваем Service Locator с этим экземпляром.

187
Если это сделано, прежде чем
ProductService используется в первый раз,
ProductService будет использовать настроенный
Stub
, чтобы работать с
ProductRepository
. В производственном приложении Service Locator будет настроен с правильной реализацией
ProductRepository в Com position Root.
Этот способ обнаружения зависимостей из класса
ProductService определенно работает, если наш единственный критерий успеха заключается в том, что зависимость может быть использована и заменена по желанию, но это имеет некоторые другие серьезные недостатки.
Анализ
Service Locator является опасным паттерном, потому что он почти работает. Мы можем обнаружить зависимости из потребляющих классов, и мы можем заменить эти зависимости различными реализациями – даже поддельными объектами из юнит тестов.
Если мы применим модель анализа, изложенну ю в главе 1, чтобы оценить, соответствует ли Service Locator преимуществам модульной конструкции приложении, мы увидим, что в основном он соответствует:

У нас есть поддержка для позднего (от ложенного) связывания при смене регистрации.

Возможна параллельная разработ ка, потому что мы программируем, используя интерфейсы, и можем по желанию заменить модули.

Мы можем достичь хорошего разделения понятий, поэтому нас ничего не останавливает от написания поддерживаемого кода. Но это все сложнее сделать.

Мы можем заменить зависимости дублирующими элементами, поэтому тестируемость возможна.
Существует только одна область, где Service Locator не дотягивает.
Влияние
Основная проблема с Service Locator заключается в том, что он влияет на повторное использование классов, которые его потребляют. Это проявляется в двух направлениях:

Модуль потянет за собой избыточную зависимость.

Это не очевидно, что используется DI.
Давайте сначала посмотрим на граф зависимостей для
ProductService из примера в разделе 5.4.1, который показан на рисунке 5-8. В дополнение к ожидаемой ссылке на абстрактный класс
ProductRepository
,
ProductService также зависит от класса
Locator
Рисунок 5-8: Граф зависимости для реализации
ProductService
, которая использует
Service Locator, чтобы обработать экземпляры абстрактного класса
ProductRepository

188
Это означает, что для повторного использования класса
ProductService
, мы должны перераспределить не только его релевантную зависимость
ProductRepository
, а также зависимость
Locator
, которая существует только по механическим причинам. Если класс
Locator определен в другом модуле, чем
ProductService и
ProductRepository
, новые приложения, которые желают повторно использовать
ProductService
, должны также принять и этот модуль.
Чтобы понять, в чем дело, представьте, что новое приложение, повторно использующее
ProductService
, уже использует другую DI стратегию, сосредотачиваясь на внедрении в конструктор.
ProductService не вписывается в эту стратегию, но вместо этого навязывает свою собственную стратегию, которая сильно загрязняет DI архитектуру нового приложения. Чтобы это использовать, разработчики должны принять существование
Service Locator, и после того как он введен, он может быть случайно использован начинающим разработчикам для других целей, где существуют лучшие альтернативы.
Возможно, мы могли даже просто перетерпеть эту дополнительну ю зависимость для
Locator
, если бы это было действительно необходимо для работы DI – мы бы рассматривали это в качестве налога, подлежащего уплате, чтобы получить другие преимущества. Тем не менее, есть лучшие варианты (например, внедрение в конструктор), так что эта зависимость является избыточной.
Чтобы добавить соли на рану, ни эта избыточная зависимость, ни ее соответствующий дубликат,
ProductRepository
, явно не видны разработчикам, желающим использовать класс
ProductService
. Рисунок 5-9 показывает, что Visual Studio не может предложить никаких рекомендаций по использованию этого класса.
Рисунок 5-9: Единственная вещь, которую нам может сказать IntelliSense о классе
ProductService
, это то, что у него есть конструктор по умолчанию. Его зависимости невидимы.
Когда мы хотим создать новый экземпляр класса
ProductService
, Visual Studio может сказать нам только, что класс имеет конструктор по умолчанию. Однако если мы впоследствии попытаемся запустить код, который мы только что написали, мы получим сообщение об ошибке времени выполнения, если мы забыли зарегистрировать экземпляр
ProductRepository при помощи класса
Locator
. Это может произойти, если мы хорошо не знаем класс
ProductService
С овет
Представьте себе, что код, который мы пишем, попадает в незадокументированный, туманный
.dll
. Как легко его может использовать кто-то еще? Это возможно – разработать API, которые близки к самодокументированию, и хотя для этого требуется практика, это достойная цель.

189
П римечание
Проблема с Service Locator заключается в том, что любой клиент, использующий его, не знает о его уровне сложности. Он выглядит простым, но оказывается сложным – и мы этого не узнаем, пока не получим исключение времени выполнения.
Проблема с классом
ProductService заключается в том, что он далек от самодокументируемости: мы не можем сказать, какая зависимость должна присутствовать, прежде чем он будет работать. На самом деле, разработчики
ProductService могут даже решить добавить несколько зависимостей в будущие версии, поэтому код, который работает в текущем варианте, может не сработать в будущей версии, и мы даже не получим ошибку компиляции, которая предупредит нас. С Service
Locator легко можно случайно ввести критические изменения.
Внимание
Использование дженериков может ввести вас в заблуждение, что Service Locator строго типизирован. Однако даже API, как в листинге 5-5, слабо типизированный, потому что мы можем запросить любой тип. Возможность компилироват ь код при помощи вызова метода
GetService
не дает нам никакой гарантии, что он не будет выбрасывать исключения налево и направо во время выполнения.
П римечание
При модульном тестировании у нас есть дополнительная проблема, что тестирующий дублер, зарегистрированный в одном тесте, вызовет взаимозависимые тесты
(Interdependent Tests), потому что он останется в памяти, когда будет выполнен следующий тест. Поэтому необходимо использовать методы тестовых фикстур (Fixture
T eardown) после каждого теста, вызывая
Locator.Reset()
, и мы должны помнить все время, что это делается вручную, а это легко забыть.
Это все действительно плохо. Service Locator может показаться безобидным, но это может привести ко всяким неприятным ошибкам выполнения. Как избежать этих проблем?
Ре факторинг по направлению к DI
Когда мы решаем избавиться от Service Locator, мы должны найти способ сделать это. Как всегда, главной альтернативой по умолчанию должно быть внедрение в конструктор, если ни один из других DI паттернов из главы 4 не подходит лучше.
Внимание
Когда мы смотрим на структуру Service Locator, она близка к Am bient Cont ext. Оба неявно используют Одиночки (Singletons) но разница заключается в наличие Local Default.
Ambient Context гарантирует, что он всегда может предоставить соответствующий экземпляр запрошенного сервиса (как правило, имеется только один). А Service Locator не может дать такую гарантию, потому что он, в сущности, является слабо типизированным контейнером сервисов, о которых он не имеет встроенных знаний.
Во многих случаях класс, который потребляет Service Locator, может вызываться по всему коду. В таких случаях он действует в качестве замены для ключевого слова new
. Если это

190 так, первый шаг рефакторинга заключается в закреплении создания каждой зависимости в одном методе.
Если мы не будем иметь поле для хранения экземпляра зависимостей, мы можем ввести такое поле и убедитесь, что остальная часть кода использует это поле, когда потребляет зависимость. Отметьте поле как readonly
, чтобы оно не могло быть изменено за пределами конструктора. Это заставляет нас присваивать значения полю из конструктора при помощи Service Locator. Теперь мы можем ввести параметр конструктора, который присваивает значение полю, вместо Service Locator, который затем может быть удален.
Представляя параметр зависимости конструктору, можно нарушить работу существующих потребителей, поэтому мы также должны справиться с этим и переместить продвижение всех зависимостей в Composition Root.
Рефакторинг класса, который использует Service Locator, похож на рефакторинг класса, который использует Control Freak, потому что Service Locator – это просто окольный вариант Cont rol Freak. Раздел 5.1.3 содержит дополнительные заметки о рефакторинге реализациий Control Freak к использованию DI.
На первый взгляд, Service Locator может выглядеть как настоящий DI паттерн, но не обманывайте себя: он может решить вопрос слабой связанности, но он создает другие проблемы на этом пути. DI паттерны, представленные в главе 4, предлагают лучшие альтернативы с меньшим количеством недостатков. Это верно как для анти-паттерна
Service Locator, так и для других анти-паттернов, представленных в этой главе. Даже если они разные, все они имеют общую черту – решить создаваемые ими проблемы можно с помощью одного из DI паттернов из главы 4.

191 5.5. Резюме
Поскольку DI представляет собой набор паттернов и технических приемов, ни один инструмент не может механически проверить, правильно ли мы их применили. В главе 4 мы рассмотрели паттерны, которые описывают, как DI можно использовать должным образом, но это только одна сторона медали. Важно также понимать, где можно наделать ошибок, даже если имеешь лучшие намерения. Есть важные уроки, которые можно извлечь из неудач, но мы не всегда должны учиться на собственных ошибках, иногда мы можем учиться на ошибках других людей.
В этой главе я описал наиболее распространенные DI ошибки в виде анти-паттернов. Я видел все эти ошибки в реальной жизни более чем один раз, и я признаю себя виновным по всем этим пунктам:

Меня зовут Марк Симан, и я использовал Control Freak.

Меня зовут Марк Симан, и я использовал Bastard Injection.

Меня зовут Марк Симан, и я использовал Constrained Construction.

Меня зовут Марк Симан, и я использовал Service Locator.
К счастью, я давно избавился от этих привычек. Я чист уже много лет.
Первая и самая опасная привычка, от которой стоит избавиться, это мнимая необходимости иметь прямой контроль над зависимостями. Это легко – обнаружить
Control Freak: каждое место, когда вы используете ключевое слово new (в C #, по крайней мере), чтобы создать экземпляр изменчивой зависимости, вы являетесь Control Freak, и не имеет значения, сколько слоев фабрик вы используете, чтобы скрыть этот факт.
Единственное место, где можно использовать ключевое слово new для зависимостей, это
Composition Root.
Избавиться от Cont rol Freak – это наиболее важная задача на сегодняшний день. Т олько тогда, когда вам удалось отсеять экземпляры Control Freak, вам стоит обратить внимание на другие анти-паттерны: они гораздо менее разрушительные.
С овет
Control Freak мешает вам использовать слабую связанность, другие же DI анти-паттерны просто делают использование неудобным, поэтому я сразу акцентирую ваше внимание на
Control Freak.
Bastard Injection разрешает DI, но потом портит партию, увлекая за собой избыточные зависимости. К счастью, реализацию Bastard Injection легко изменить при помощи внедрения в конструктор, поэтому никакой необходимости существовать с Bastard
Injection нет. М ы получаем больше, чем теряем, при переходе к правильному решению: на самом деле, мы только теряем время, необходимое для выполнения рефакторинга.
Constrained Construction накладывает искусственные ограничения на типы, которые мы используем для реализации абстракций. В большинстве случаев это принимает форму ограничения всех реализациях, чтобы иметь конструктор по умолчанию, но в некоторых случаях конструкторы должны принять определенные параметры для инициализации компонента.

192
Вы должны снять эти ограничения и использовать DI контейнер или вручную внедрить в объекты необходимые зависимости, где бы они не были. Если у вас есть сценарий, где необходимо инициализиро ват ь некоторые компоненты при помощи информации о текущем контексте, нужно применять внедрение в метод.
Service Locator может выглядеть привлекательно, но я считаю его анти-паттерном, хотя это несколько спорное мнение. Несмотря на то, что он решает некоторые проблемы DI, он вводит другие проблемы, которые перевешивают его преимущества. Нет никаких причин принимать его недостатки, поскольку DI паттерны, представленные в главе 4, предлагают лучшие альтернативы. Это общая тема для всех анти-паттернов, описанных в этой главе:
DI паттерны из главы 4 предлагают решения проблем, вызванных анти-паттернами.
Сейчас уже вы должны знать, чего следует избегать и что вы в идеале должны делать вместо этого, но все же есть проблемы, которые выглядят так, как будто их трудно решить. В следующей главе обсуждаются такие проблемы и их решения.

193 6. DI ре факторинг
Меню

Преобразование динамических значений в абстракции

Работа с недолговечными зависимостями

Разрешение циклических зависимостей

Работа с Constructor Over-injection

Мониторинг связывания
Вы могли заметить, что мне нравится sauce béarnaise или sauce hollandaise в целом. Одной из причин является то, что они очень хороши на вкус, а другая заключается в том, что их довольно сложно сделать. В дополнение к проблеме приготовления sauce hollandaise дает нам еще одно испытание: он должен быть подан немедленно (или мне так кажется).
Раньше это было далеко от идеала, когда у меня были гости. Вместо того, чтобы приветствовать своих гостей и заставить их чувствовать себя желанными и расслабленными, я отчаянно взбивал соус на кухне, оставив их развлекать самих себя.
После нескольких повторных выступлений, моя очень общительная жена решила взять дело в свои руки. Мы живем через дорогу от ресторана, так что в один прекрасный день она разговорила поваров, чтобы выяснить секрет, который позволил бы мне приготовить подлинный голландский соус заблаговременно. Оказывается, что такой секрет есть, так что теперь я могу сделать вкусный соус для моих гостей, не подвергая их в атмосферу стресса и безумия.
Каждый имеет свои хитрости. Это также верно для разработки программного обеспечения в целом и, в частности, DI. Есть проблемы, которые просто продолжают появляться, и во многих случаях существуют хорошо известные способы борьбы с ними.
На протяжении многих лет я видел, как люди прилагают все усилия для изучения DI, и мне пришло в голову, что многие из вопросов похожи по своей структуре. В этой главе мы рассмотрим наиболее распространенные проблемы, которые появляются, когда мы применяем DI к коду, и как мы можем решить их. Когда мы закончим, вы должны быть в состоянии лучше распознавать и решать такие проблемы, когда они появляются.
Как и две предыдущие главы этой части книги, эта глава организована в виде каталога – на этот раз проблем и решений (или, если угодно, рефакторинга). Рисунок 6-1 показывает структуру главы.
Рисунок 6-1: Структура этой главы представляет собой каталог способов рефакторинга и решений обычных DI проблем. Каждый раздел является независимым.

194
В каждом разделе я представлю общую проблему и ее решение, в том числе на примере.
Вы можете прочитать каждый раздел самостоятельно или в последовательно сти, как вам удобнее. Цель каждого раздела заключается в ознакомлении с решением часто встречающихся проблем, в том, что вы будете лучше оснащены для борьбы с ними, если вы с ними столкнетесь.
6.1. Преобразование динамических значений в абстракции
6.2. Работа с недолговечными зависимостями
6.3. Разрешение циклических зависимостей
6.4. Обсуждение феномена Constructor Over-injection
6.5. Мониторинг связывания
6.6. Резюме

195 6.1. Преобразование динамических значений в абстракции
Когда вы начинаете применять DI, одна из первых трудностей, с которой вы, вероятно, столкнетесь, заключается в том, что абстракции зависят от значений времени выполнения.
Например, сайт с онлайн картой может предложить рассчитать маршрут между двумя точками. Это может дать вам выбор по расчету маршрута: вы хотите кратчайший путь?

1   ...   9   10   11   12   13   14   15   16   ...   43


написать администратору сайта