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

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

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

Марк Симан

13.4. Регистрация сложных API

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

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

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

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

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

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

public ChiliConCarne(Spiciness spiciness)

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

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

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

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

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

builder.Register<Spiciness>(c => Spiciness.Medium);
builder.RegisterType<ChiliConCarne>().As<ICourse>();

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

Если вы будете достаточным образом контролировать взаимосвязь Spiciness и ChiliConCarne, вы сможете использовать метод WithParameter таким же образом, как и в листингах 13-4, 13-5, 13-6:

builder.RegisterType<ChiliConCarne>()
	.As<ICourse>()
	.WithParameter("spiciness", Spiciness.Hot);

Поскольку вы собираетесь передать в параметр spiciness конкретное значение, вы можете воспользоваться другой перегрузкой метода WithParameter, которая в качестве входных данных принимает имя и значение параметра. Эта перегрузка делегирует полномочия другому WithParameter путем создания экземпляра NamedParameter из имени и значения параметра. NamedParameter также наследуется от Parameter, как и ResolvedParameter.

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

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

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

builder.Register<ICourse>(c =>
	new ChiliConCarne(Spiciness.Hot));

Вы уже видели метод Register, когда мы обсуждали Decorator'ы в разделе 13.3.3 "Интеграция Decorator'ов". Всякий раз при разрешении компонента ICourse будет вызываться конструктор ChiliConCarne с параметром Spiciness.Hot.

Примечание

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

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

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

internal JunkFood(string name)

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

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

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

builder.Register(c =>
	JunkFoodFactory.Create("chicken meal"));

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

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

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

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

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

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

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

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

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

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

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

builder.RegisterType<CaesarSalad>()
	.As<ICourse>()
	.PropertiesAutowired();
builder.RegisterType<Chicken>().As<IIngredient>();

Поскольку метод PropertiesAutowired является частью простого API регистрации, вы можете вызвать его для того, чтобы сообщить Autofac, что ему необходимо автоматически интегрировать доступные для записи свойства класса CaesarSalad. Autofac будет автоматически интегрировать только те свойства, о способе заполнения которых он осведомлен, поэтому вы также регистрируете Chicken в виде IIngredient. Если бы вы это не сделали, то свойство Extra было бы проигнорировано.

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

Если вам нужен более подробный контроль, нежели тот, который предоставляется посредством метода PropertiesAutowired, то вы можете воспользоваться методом WithProperty, который похож на метод WithParameter, используемый вами ранее:

builder.RegisterType<Chicken>().As<IIngredient>();
builder.RegisterType<CaesarSalad>()
	.As<ICourse>(
	.WithProperty(new ResolvedParameter(
		(p, c) => p.Member.Name == "set_Extra",
		(p, c) => c.Resolve<IIngredient>()));

Метод WithProperty отражает уже полюбившийся вам метод WithParameter: он принимает в качестве входных данных только один аргумент Parameter и также обладает перегрузкой, которая в качестве параметров принимает имя и значение свойства.

Чтобы должным образом разрешить метод Extra, можно воспользоваться доверенным классом ResolvedParameter. Что касается свойств, переданный нами предикат имеет небольшую отличительную черту, поскольку Autofac вызывает блок кода с аргументом ParameterInfo, а не с PropertyInfo. Параметр p олицетворяет параметр value, который всегда потенциально доступен при реализации свойства, поэтому нам необходимо перейти к Member, который определяет этот параметр. Member – это экземпляр MethodInfo, поэтому нам нужно ознакомиться с тем, как реализуются C# свойства на уровне интерфейса: в действительности свойства Extra – это метод под названием set_Extra.

Когда предикат передан, легко реализовать получение значения путем разрешения IIngredient из переданного IComponentContext.

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

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

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

builder.RegisterType<Chicken>().As<IIngredient>();
builder.RegisterType<CaesarSalad>()
	.As<ICourse>()
	.OnActivating(e =>
		e.Instance.Extra = e.Context.Resolve<IIngredient>());

Метод OnActivating дает вам возможность выполнить какое-нибудь действие над компонентом, пока Autofac не вернет его тому объекту, который его запрашивает. В качестве единственного параметра он принимает Action<IActivatingEventArgs<CaesarSalad>>, который можно использовать для реализации выбранной вами логики постобработки. Параметр e олицетворяет аргументы события, а также обладает свойством Instance типа CaesarSalad и свойством Context, которое можно использовать для разрешения других компонентов. Это сочетание вы используете для того, чтобы разрешить IIngredient и вернуть результат в свойство Extra. При разрешении ICourse вы получите экземпляр CaesarSalad, свойство Extra которого имеет значение Chicken.

Поскольку свойство Instance привязано к аргументу интерфейса IActivatingEventArgs<T>, имеющему generic-тип, этот подход является строго типизированным и влечет за собой свои преимущества и недостатки.

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

В целом Property Injection отлично поддерживается и механизмом автоматической интеграции и строго типизированными присваиваниями.