Главная страница   /   10.4. Конфигурирование сложных API (Внедрение зависимостей в .NET

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

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

Марк Симан

10.4. Конфигурирование сложных API

До настоящего момента мы рассматривали то, как можно конфигурировать компоненты, использующие Constructor Injection. Одним из главных преимуществ Constructor Injection является то, что DI-контейнеры, например, Castle Windsor, могут с легкостью понимать, как компоновать и создавать все классы диаграммы зависимостей.

Все становится менее понятным, когда API не столь хорошо функционируют. В данном разделе вы увидите, как работать с простыми аргументами конструктора, статическими фабриками и Property Injection. Все это требует особого внимания. Давайте начнем с рассмотрения классов, которые принимают в качестве параметров простыми типы, например, строки и целые числа.

Конфигурирование простыми зависимостей

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

В сущности, регистрация строкового или числового типа в качестве компонента контейнера не имеет особого смысла, а в Castle Windsor это и вовсе не работает. Если мы попытаемся разрешить компонент с простой зависимостью, мы получим исключение, даже если простой тип был до этого зарегистрирован.

Рассмотрите в качестве примера приведенный ниже конструктор:

public ChiliConCarne(Spiciness spiciness)

В этом примере Spiciness имеет перечисляемый тип:

public enum Spiciness
{
	Mild = 0,
	Medium,
	Hot
}

Предупреждение

Согласно эмпирическому правилу перечисления являются "запахами" и их нужно преобразовать в полиморфные классы (имеющие разное состояние). Тем не менее, для данного примера они вполне нам подходят.

Необходимо явным образом сообщить Castle Windsor о том, как разрешать параметр конструктора spiciness. Следующий листинг демонстрирует, как это можно сделать, используя синтаксис, очень похожий на метод ServiceOverrides, но представляющий собой другой метод.

Листинг 10-10: Применение простого значения для аргумента конструктора
container.Register(Component
	.For<ICourse>()
	.ImplementedBy<ChiliConCarne>()
	.DependsOn(new
		{
			spiciness = Spiciness.Hot
		}));

Вместо метода ServiceOverrides, который переопределяет автоматическую интеграцию, можно использовать метод DependsOn, который дает возможность применять экземпляры конкретных зависимостей. В данном примере для параметра конструктора spiciness вы используете значение Spiciness.Hot.

Примечание

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

Предупреждение

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

Всякий раз, когда нам нужно использовать простое значение, например, строку соединения, мы можем явным образом определить значение в коде (или взять его из конфигурации приложения) и присвоить его при помощи метода DependsOn. Что хорошо при использовании DependsOn, так это то, что нам не нужно явным образом вызывать конструктор или применять какие-либо другие зависимости, в которых автоматическая интеграция была бы более подходящей. Но недостаток использования DependsOn – он более хрупок для выполнения рефакторинга.

Существует более мощная альтернатива, которая позволяет явным образом вызывать конструктор. Она также может использоваться для работы с классами, которые не имеют традиционных конструкторов.

Регистрация компонентов с помощью блоков кода

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

Рассмотрите приведенный ниже пример конструктора класса JunkFood:

internal JunkFood(string name)

Даже если класс JunkFood является открытым, конструктор расположен внутри него. Очевидно, экземпляры JunkFood должны создаваться посредством статического класса JunkFoodFactory:

public static class JunkFoodFactory
{
	public static IMeal Create(string name)
	{
		return new JunkFood(name);
	}
}

С точки зрения Castle Windsor это проблемное API, поскольку в нем отсутствуют точно выраженные и заданные соглашения касательно статических фабрик. Тут требуется помощь – и мы можем предоставить ее посредством блока кода, что продемонстрировано в следующем листинге.

Листинг 10-11: Конфигурирование метода фабрики
container.Register(Component
	.For<IMeal>()
	.UsingFactoryMethod(() =>
		JunkFoodFactory.Create("chicken meal")));

Для определения блока кода, который создает соответствующий экземпляр, можно использовать метод UsingFactoryMethod – в данном примере путем вызова метода Create с необходимым параметром для JunkFoodFactory.

Данный блок кода будет вызываться в соответствующее время согласно сконфигурированному стилю существования компонента. В данном примере, поскольку вы явным образом не определили стиль существования, по умолчанию используется Singleton, а метод Create будет вызываться всего единожды, вне зависимости от того, сколько раз вы разрешаете IMeal. Если бы вы сконфигурировали компонент таким образом, чтобы для него использовался стиль существования Transient, то метод Create вызывался бы всякий раз, когда вы разрешали бы IMeal.

Использование блока кода предоставляет возможность более экзотичной инициализации объектов, нежели обычные открытые конструкторы. Кроме того, используя блок кода, мы получаем обеспечивающую наибольшую типовую безопасность альтернативу применению простых типов, нежели та, которую предоставляет метод DependsOn, который вы наблюдали в разделе 10.4.1 "Конфигурирование простых зависимостей":

container.Register(Component
	.For<ICourse>()
	.UsingFactoryMethod(() =>
		new ChiliConCarne(Spiciness.Hot)));

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

Подсказка

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

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

Последним общепринятым отклонением от Constructor Injection, которое мы будем здесь наблюдать, является Property Injection.

Подключение с помощью Property Injection

Property Injection является не столь четко определенной формой механизма внедрения зависимостей, поскольку компилятор не принуждает вас присваивать значение свойству, доступному для записи. И все-таки Castle Windsor, по своей природе, понимает Property Injection и по возможности присваивает значения доступным для записи свойствам.

Рассмотрите приведенный ниже класс CaesarSalad:

public class CaesarSalad : ICourse
{
	public IIngredient Extra { get; set; }
}

Согласно общепринятому заблуждению в салат "Цезарь" входит курица. По существу салат "Цезарь" является салатом, но, поскольку с курицей он вкуснее, многие рестораны предлагают возможность добавления в него курицы в качестве дополнительного ингредиента. Класс CaesarSalad моделирует такую возможность посредством доступного для записи свойства под названием Extra.

Если вы зарегистрируете только класс CaesarSalad без какого-либо Chicken, то свойству Extra не будет присвоено значение:

container.Register(Component
	.For<ICourse>()
	.ImplementedBy<CaesarSalad>());

Благодаря такой регистрации в результате разрешения ICourse будет возвращаться экземпляр CaesarSalad без какого-либо ингредиента Extra. Тем не менее, вы можете изменить выходной результат, добавив в контейнер Chicken:

container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<Chicken>());
container.Register(Component
	.For<ICourse>()
	.ImplementedBy<CaesarSalad>());

Теперь при разрешении ICourse свойство Extra возвращаемого экземпляра CaesarSalad будет представлять собой экземпляр класса Chicken. То есть, Castle Windsor просматривает новый экземпляр на наличие доступных для записи свойств и присваивает им значения, если может предоставить компонент, который по типу совпадает с типом свойства.

Подсказка

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

В данном разделе вы увидели, как работать с API, которые отклоняются от Constructor Injection. Вы можете обратиться к простым аргументам конструктора с помощью метода DependsOn или UsingFactoryMethod, который также поддерживает методы фабрики и другие альтернативы открытых конструкторов. Castle Windsor, по своей природе, поддерживает Property Injection.