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

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

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

Марк Симан

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

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

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

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

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

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

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

public ChiliConCarne(Spiciness spiciness)

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

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

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

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

Необходимо явным образом сообщить StructureMap о том, как разрешать параметр конструктора spiciness. Приведенный ниже пример демонстрирует, как можно использовать метод Ctor<T> для того, чтобы явным образом предоставить значение для параметра конструктора:

container.Configure(r => r
	.For<ICourse>()
	.Use<ChiliConCarne>()
	.Ctor<Spiciness>()
	.Is(Spiciness.Hot));

В разделе 11.3 "Работа с составными компонентами" вы не раз видели, каким образом можно использовать метод Ctor<T> для того, чтобы переопределить автоматическую интеграцию для конкретного параметра конструктора. В данном разделе вы косвенным образом устанавливаете, что, подразумевается, что конструктор ChiliConCarne имеет только один параметр Spiciness. В противном случае вызов метода Ctor<spiciness>() будет неоднозначным, и вам придется передавать также и имя параметра.

Метод Ctor<T> возвращает SmartInstance<T>, который имеет разнообразные методы. Существует 5 перегрузок метода Is, а одна из них дает возможность предоставить экземпляр соответствующего типа. Аргументом типа T в данном случае является Spiciness, поэтому вы предоставляете Spiciness.Hot в качестве конкретного значения.

Как мы уже обсуждали в разделе 11.3 "Работа с составными компонентами", использование метода Ctor<T> имеет свои преимущества и недостатки. Если нам нужна более строго типизированная конфигурация, которая вызывает конструктор или статическую фабрику, мы также можем это сделать.

Создание объектов с помощью блока кода

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

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

internal JunkFood(string name)

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

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

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

container.Configure(r => r
	.For<IMeal>()
	.Use(() =>
		JunkFoodFactory.Create("chicken meal")));

К этому времени цепочка методов For/Use должна быть вам уже знакома. Тем не менее, в данном случае вы используете перегрузку метода Use, отличную от той, которую вы использовали ранее. Эта перегрузка позволяет вам передавать Func<IMeal>, что вы делаете посредством блока кода, который вызывает статический метод Create класса JunkFoodFactory.

Подсказка

Если вы хотите разрешить класс ChiliConCarne из раздела 11.4.1 "Конфигурирование простейших зависимостей" строго типизированным способом, то можете использовать данную перегрузку Use для непосредственного вызова конструктора.

После завершения написания кода, который создает экземпляр, можете ли вы ответить, почему такой подход в любом случае лучше непосредственного вызова кода? Используя блок кода внутри оператора For/Use, вы кое-что, таким образом, приобретаете:

  • Преобразуете IMeal к JunkFood.
  • Стиль существования все еще можно конфигурировать. Несмотря на то, что для создания экземпляра будет вызываться блок кода, он может и не вызываться всякий раз, когда запрашивается данный экземпляр. За исключением тех случаев, когда вы используете стиль существования Unique, иногда вместо блока кода может использоваться кэшированный экемпляр.

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

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

Интеграция с помощью Property Injection

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

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

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

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

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

Существует несколько способов, с помощью которых можно сконфигурировать CaesarSalad таким образом, чтобы свойство Extra заполнялось соответствующим образом. Один из таких способов – использование ссылок на экземпляры. Этот способ вы уже ранее несколько раз видели в данной главе:

container.Configure(r =>
{
	var chicken = r.For<IIngredient>().Use<Chicken>();
	r.For<ICourse>().Use<CaesarSalad>()
		.Setter<IIngredient>().Is(chicken);
});

Из нескольких предыдущих примеров вы можете вспомнить, что метод Use возвращает Instance, который вы можете помнить как переменную. В листинге 11-10 и во многих последующих примерах вы использовали метод Ctor<T> для того, чтобы обозначить параметр конструктора определенного типа. Метод Setter<T> работает аналогичным образом, но только для свойств. Вы передаете экземпляр chicken в метод Is, чтобы заставить StructureMap присвоить значение свойству при построении экземпляра.

Когда вы будете на основании этой конфигурации разрешать ICourse, вы получите обратно экземпляр CaesarSalad, свойству Extra которого будет присвоен экземпляр Chicken. Это предоставляет вам возможность дифференцированного управления конкретными свойствами конкретных типов. API, которое в большей степени основано на соглашениях, предоставляет нам возможность утверждать, что мы хотим, чтобы StructureMap использовало все свойства данного типа для Property Injection. К примеру, мы могли бы установить, что все заданные свойства IIngredient должны внедряться вместе с соответствующим экземпляром.

В случае CaesarSalad вы можете выразить это следующим образом:

container.Configure(r =>
	r.For<IIngredient>().Use<Chicken>());
container.Configure(r =>
	r.For<ICourse>().Use<CaesarSalad>());
container.Configure(r =>
	r.FillAllPropertiesOfType<IIngredient>());

Благодаря методу FillAllPropertiesOfType вы можете установить, что всем доступным для записи свойствам типа IIngredient должно быть присвоено значение. StructureMap будет использовать экземпляр по умолчанию, сконфигурированный для IIngredient, поэтому при разрешении ICourse вы получите экземпляр CaesarSalad со свойством Extra равным Chicken.

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

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