Главная страница   /   5.3. Constrained Construction (Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Марк Симан

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
string connectionString =
	ConfigurationManager.ConnectionStrings
	["CommerceObjectContext"].ConnectionString;
string productRepositoryTypeName =
	ConfigurationManager.AppSettings
	["ProductRepositoryType"];
var productRepositoryType =
	Type.GetType(productRepositoryTypeName, true);
var repository =
	(ProductRepository)Activator.CreateInstance(
		productRepositoryType, connectionString);

Строки 9-11: Создание экземпляра конкретного типа

Первое, что должно вызвать подозрение – это то, что строка соединения считывается из файла web.config. Зачем вам нужна строка соединения, если вы планируете обрабатывать ProductRepository как абстракцию? Хотя, возможно, это и маловероятно, но вы можете захотеть реализовать ProductRepository с базой данных в памяти или XML файлом. REST-сервис хранения данных, такой как Windows Azure Table Storage Service предлагает более реалистичную альтернативу, но в очередной раз самым популярным выбором, кажется, остаются реляционные базы данных. Повсеместное распространение баз данных ведет к тому, что слишком легко забыть, что строка соединения неявно представляет выбор реализации.

Чтобы сделать позднюю привязку ProductRepository, вы должны определить, какой тип был выбран в качестве реализации. Это можно сделать, прочитав имя типа, определенное сборкой, из web.config и создав экземпляр типа с таким именем. Это само по себе не является проблемой – трудность возникает только тогда, когда вам нужно создать экземпляр этого типа.

С наличием Type вы можете создать экземпляр с помощью класса Activator. Метод CreateInstance вызывает конструктор типа, поэтому вы должны передать верные параметры конструктору, чтобы предотвратить исключение. В этом случае нужно указать строку соединения.

Если бы вы ничего не знали о приложении, только видели код в листинге 5-3, то вы бы удивились, почему строка соединения передается в качестве аргумента конструктора неизвестному типу. Это не имело бы больше смысла, если бы реализация была основана на REST веб-сервисе или XML файле.

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

Примечание

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

Можно утверждать, что ProductRepository на основе XML файла также потребует строку в качестве параметра конструктора, хотя этой строкой будет имя файла, а не строка соединения. Тем не менее, концептуально это все равно будет странно, потому что вам все равно нужно было бы определить это имя файла в элементе connectionStrings в web.config (и в любом случае, я думаю, что такой гипотетический XmlProductRepository должен принять XmlReader в качестве аргумента конструктора, а не имя файла).

Моделирование конструкции зависимости исключительно на явных ограничениях (интерфейса или базового класса) является намного более хорошим и более гибким вариантом.

Анализ

В предыдущем примере неявное ограничение требует от реализующих элементов иметь конструктор с одним строковым параметром. Более распространенным ограничением является то, что все реализации должны иметь конструктор по умолчанию, так чтобы работала простейшая форма Activator.CreateInstance:

var dep = (ISomeDependency)Activator.CreateInstance(type);

Хотя это можно назвать наименьшим общим знаменателем, цена гибкости слишком высока.

Влияние

Независимо от того, как мы ограничиваем строение объекта, мы теряем гибкость. Может возникнуть соблазн заявить, что все реализации зависимостей должны иметь конструктор по умолчанию – в конце концов, они могли бы выполнять свою инициализацию внутренне, например, чтением конфигурационных данных, таких как конфигурационные строки, непосредственно из файла .config. Однако это ограничило бы нас по-другому, потому что мы, возможно, захотели бы иметь возможность компоновать приложение слоями экземпляров, которые включают другие экземпляры. В некоторых случаях, например, мы могли бы захотеть распределить экземпляры между различными потребителями, как показано на рисунке 5-4.

Рисунок 5-4: В этом примере мы хотим создать единственный экземпляр класса ObjectContext и внедрить этот же экземпляр в оба репозитория. Это возможно только в том случае, если мы можем внедрить экземпляр извне.

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

Примечание

Только потому, что DI позволяет нам делиться одним экземпляром среди многих потребителей, не означает, что мы всегда должны это сделать. Совместное использование экземпляра экономит память, но может создать проблемы взаимодействия, например, проблем многопоточности. Решение о том, хотим мы поделиться экземпляром или нет, тесно связано с концепцией жизненного цикла объекта, которая обсуждается в главе 8.

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

Рефакторинг по направлению к DI

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

Внимание

Хотя мы можем использовать абстрактные фабрики, чтобы успешно реализовать позднее связывание, это требует дисциплины. В общем, нам будет лучше с надлежащим DI контейнером; но я, тем не менее, покажу, как это сделать.

Давайте кратко рассмотрим такой подход. Представьте себе, что у вас есть абстракция сервиса, образно называемая ISomeService. Схема абстрактной фабрики подсказывает, что вам также нужен интерфейс ISomeServiceFactory. Рисунок 5-5 иллюстрирует эту структуру.

Рисунок 5-5: ISomeService представляет реальную зависимость. Однако чтобы сохранить реализующие элементы свободными от неявных ограничений, вы пытаетесь разрешить вопрос поздней связанности путем введения ISomeServiceFactory, которая будет использоваться для создания экземпляров ISomeService. И вам потребуется любая фабрика, поскольку у нее есть конструктор по умолчанию.

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

Листинг 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, чтобы провести именно эту комбинацию, а затем настроить приложение для использования этой фабрики вместо предыдущей. Это означает, что вы не можете определить произвольные комбинации зависимостей без написания и компиляции кода, но вы можете сделать это без перекомпиляции самого приложения.

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

Анти-паттерн Constrained Construction применяется действительно только тогда, когда мы используем позднее связывание, потому что когда мы используем раннее связывание, компилятор гарантирует, что мы никогда не введем неявные ограничения в том, как строятся компоненты.

Последний анти-паттерн применяется более часто – некоторые люди даже считают его настоящим паттерном, а не анти-паттерном.