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

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

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

Марк Симан

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

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

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

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

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

В сущности, регистрация строкового или числового типа в качестве компонента контейнера не имеет особого смысла. Но в рамках Unity это, по крайней мере, осуществимо.

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

public ChiliConCarne(Spiciness spiciness)

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

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

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

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

Если вы хотите, чтобы все потребители Spiciness использовали одно и то же значение, можно зарегистрировать Spiciness и ChiliConCarne независимо друг от друга:

container.RegisterInstance(Spiciness.Medium);
container.RegisterType<ICourse, ChiliConCarne>();

Когда вы впоследствии будете разрешать ChiliConCarne, его Spiciness будет иметь значение Medium, как и все остальные компоненты, зависимые от Spiciness.

Если вы будете достаточным образом контролировать взаимосвязь Spiciness и ChiliConCarne, то сможете таким же образом поступить и с вездесущим InjectionConstructor, передав значение в конструктор:

container.RegisterType<ICourse, ChiliConCarne>(
	new InjectionConstructor(Spiciness.Hot));

Ранее в этой главе InjectionConstructor в основном использовался с ResolvedParameter<T>, но можно поступить и по-другому: передать значение, которое затем будет передано прямо в конструктор компонента. В приведенном примере вы передаете значение Spiciness.Hot, которое затем будет передано прямо в конструктор ChiliConCarne, возвращая значение Hot.

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

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

Еще один вариант создания компонента с примитивным значением – использовать еще один InjectionMember, позволяющий передавать делегат, который создает компонент:

container.RegisterType<ICourse, ChiliConCarne>(
	new InjectionFactory(
			c => new ChiliConCarne(Spiciness.Hot)));

InjectionFactory – это еще один класс, унаследованный от абстрактного класса InjectionMember. Он обладает двумя перегруженными конструкторами, но мы используем самый простой из них, который в качестве входных данных принимает Func<IUnityContainer, object>. Это дает нам возможность определить блок кода, с помощью которого будет создаваться компонент. В этом примере всякий раз при разрешении компонента ICourse будет вызываться конструктор ChiliConCarne с параметром Spiciness.Hot.

Примечание

Блок кода, приведенный в примере выше, полностью идентичен соответствующему блоку кода, приведенному для контейнера Autofac в разделе 13.4.2 "Регистрация объектов с помощью блоков кода".

Когда дело касается класса ChiliConCarne, вам предоставляется выбор между автоматической интеграцией и использованием блока кода. Но другие классы более ограничены: их экземпляры нельзя создать с помощью открытого конструктора. Чтобы создать экземпляры типа, вместо открытого конструктора вам приходится использовать некоторого рода фабрику. Для DI-контейнеров это всегда проблематично, поскольку по умолчанию им нужны открытые конструкторы.

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

internal JunkFood(string name)

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

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

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

container.RegisterType<IMeal, JunkFood>(
	new InjectionFactory(
			c => JunkFoodFactory.Create("chicken meal")));

В этот раз вы используете класс InjectionFactory для создания компонента, вызывая статическую фабрику в рамках блока кода. Всякий раз при разрешении IMeal будет вызываться JunkFoodFactory.Create и возвращаться результат.

Является ли написание блока кода для создания экземпляра лучшим вариантом, нежели прямой вызов кода? При использовании блока кода внутри конструктора InjectionFactory мы приобретаем следующие преимущества:

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

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

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

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

Рассмотрим класс CaesarSalad:

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

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

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

Помимо этого, чтобы присвоить значение свойству, вы можете использовать InjectionMember:

container.RegisterType<IIngredient, Chicken>();
container.RegisterType<ICourse, CaesarSalad>(
	new InjectionProperty("Extra"));

Точно так же, как вы можете использовать класс InjectionConstructor для конфигурирования Constructor Injection, вы можете использовать класс InjectionProperty для конфигурирования Property Injection. InjectionProperty – это еще один класс, унаследованный от InjectionMember. Чтобы его использовать, вы должны указать имя того свойства, которое необходимо заполнить. В этом примере вы собираетесь заполнить свойство Extra. Это приведет к автоматической интеграции свойства, поэтому очень важно, чтобы Unity умел разрешать этот тип. Свойство Extra имеет тип IIngredient, поэтому Unity разрешит это свойство в Chicken благодаря тому, что ранее вы зарегистрировали Chicken как IIngredient.

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

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

Когда вы на основании этой регистрации разрешаете ICourse, вы получаете экземпляр CaesarSalad, свойству Extra которого присвоен экземпляр Chicken.

В предыдущем примере использовался IIngredient по умолчанию, но для того чтобы присвоить значение свойству, мы можем использовать и другую перегрузку класса InjectionProperty. Мы можем присвоить ему непосредственное значение или можем воспользоваться проверенным классом ResolvedParameter<T>, чтобы сослаться на именованный компонент:

container.RegisterType<IIngredient, Chicken>("chicken");
container.RegisterType<IIngredient, Steak>("steak");
container.RegisterType<ICourse, CaesarSalad>(
	new InjectionProperty("Extra",
			new ResolvedParameter<IIngredient>("chicken")));

Экземпляр ResolvedParameter ссылается на ранее зарегистрированный компонент chicken, гарантируя, что при разрешении ICourse вы получите CaesarSalad, свойству Extra которого присвоен экземпляр Chicken.

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