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

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

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

Марк Симан

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

До настоящего момента мы рассматривали то, как можно компоновать части, использующие Constructor Injection. Одним из главных преимуществ Constructor Injection является то, что DI-контейнеры могут с легкостью понимать, как компоновать и создавать все классы диаграммы зависимостей. В MEF, с другой стороны, необходимо явно использовать атрибут [ImportingConstructor], поэтому для MEF это не совсем справедливо.

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

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

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

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

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

public ChiliConCarne(Spiciness spiciness)

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

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

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

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

Чтобы соответствующим образом пометить ChiliConCarne, вы добавляете к конструктору атрибут [ImportingConstructor]. При экспорте Spiciness лучше всего сделать это с помощью адаптера:

public class SpicinessAdapter
{
	[Export]
	public Spiciness Spiciness
	{
		get { return Spiciness.Hot; }
	}
}

Этот адаптер экспортирует значение Spiciness.Hot таким образом, что, если вы компонуете ChiliConCarne из каталога, в котором содержатся эти части, то вы получите горячее Chili con Carne.

Подсказка

Вместо того чтобы экспортировать и импортировать сам тип Spiciness, можно использовать пользовательскую строку в качестве совместно используемого контракта. Для этого потребуется добавить дополнительный атрибут [Import] в аргумент конструктора spiciness с целью определения контракта.

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

Компоновка частей без открытых конструкторов

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

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

internal JunkFood(string name)

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

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

Предположим, что вы не можете изменить это API. Как вы тогда поступите в этой ситуации, чтобы суметь должным образом сынтегрировать и скомпоновать JunkFood? Ответ на этот вопрос точно такой же, как и для всех остальных случаев, когда вы не можете изменить первоначальный экспортированный тип: используйте адаптер подобно тому, как это продемонстрировано в следующем листинге.

Листинг 15-6: Экспорт типа, имеющего внутренний конструктор
public class JunkFoodAdapter
{
	private readonly IMeal junk;
	public JunkFoodAdapter()
	{
		this.junk = JunkFoodFactory.Create("chicken meal");
	}
	[Export]
	public IMeal JunkFood
	{
		get { return this.junk; }
	}
}

В JunkFoodAdapter инкапсулировано знание о том, что экземпляр JunkFood создается с помощью метода JunkFoodFactory.Create. Этот метод создает экземпляр в конструкторе и импортирует его через свойство. Поскольку тип свойства – IMeal, то он также является и экспортированным контрактом.

С помощью расположенного в каталоге класса JunkFoodAdapter вы можете успешно разрешать IMeal и возвращать экземпляра блюда из курицы JunkFood.

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

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

Property Injection – это менее определенная форма механизма внедрения зависимостей, поскольку компилятор не принудает нас задавать значение свойства, доступного для записи. По иронии MEF был задуман скорее как использующий паттерн Property Injection, а не Constructor Injection. Это объясняет тот факт, что нам не нужно явно применять атрибуты ко всему, что мы собираемся скомпоновать: с точки зрения MEF, паттерн Property Injection (являющийся неопределенным) является используемым по умолчанию, а Constructor Injection – менее идиоматичным вариантом.

Несмотря на то, что я считаю эту точку зрения одновременно и устаревшей, и неверной, это все же позволяет без затруднений использовать в MEF паттерн Property Injection. Все, что нам приходится делать, – применять к свойству атрибут [Import].

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

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

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

Чтобы разрешить использовать для CaesarSalad паттерн Property Injection, вам необходимо просто применить атрибут [Import]:

[Import(AllowDefault = true)]
public IIngredient Extra { get; set; }

В этой книге я последовательно рассматриваю паттерн Property Injection, который применяется в тех случаях, когда есть возможность передавать внешние зависимости. Это имеет смысл, поскольку компилятор не принуждает вас присваивать свойству значение (в отличие от аргумента конструктора). Но MEF не придерживается этой точки зрения. По умолчанию импорт должен выполняться, пока вы явно с помощью свойства AllowDefault не укажете, что делать это необязательно. Чтобы продолжать соответствовать описанному выше паттерну Property Injection, свойству AllowDefault вы присваиваете значение true. Это означает, что MEF не будет выдавать исключение, если не сможет импортировать IIngredient.

Вы должны знать, что если свойство AllowDefault имеет значение true, то вместо того, чтобы игнорировать свойство, когда импорт не может быть выполнен, MEF будет явным образом присваивать свойству значение по умолчанию (в этом случае null). Чтобы применить эту возможность, вы должны быть готовы к работе с null-значениями, но это приведет к разрушению инвариантов класса. Вы должны использовать большие значения, чтобы избежать присваивания null-значений приватным полям.

Один из способов работы с null-значениями – молча поглотить такое значение:

[Import(AllowDefault = true)]
public IIngredient Extra
{
	get { return this.extra; }
	set
	{
		if (value == null)
		{
			return;
		}
		this.extra = value;
	}
}

Строка 7-10: Молчаливое игнорирование null-значений

Вы можете явным образом проверить, имеет ли свойство значение null, и выйти, если вызывающий оператор пытается внедрить null-значение. Такое поведение приводит к нарушению принципа "наименьшего удивления" (Principle of Least Surprise), поскольку вызывающих операторов может удивить тот факт, что присваивание значения не дало никакого результата, даже если при этом не выдавалось никакого исключения. И снова вы пришли к тому, что Property Injection – очень проблематичный паттерн, и лучше всего избегать его использования, пока оно не будет оправданно.

Известно, что Property Injection характерен для MEF, но, как часто бывает, все зло кроется в деталях. Даже при работе с MEF я предпочитаю использовать Constructor Injection, мой любимый паттерн.

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