Главная страница   /   4.1. Внедрение в конструктор (Constructor Injection) (Внедрение зависимостей в .NET

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

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

Марк Симан

4.1. Внедрение в конструктор (Constructor Injection)

Как мы можем гарантировать, что необходимая зависимость всегда доступна для класса, который мы в настоящее время разрабатываем?

Запрашивая все вызывающие элементы, чтобы доставить зависимость в качестве параметра конструктору класса.

Рисунок 4-3: NeedyClass нуждается в экземпляре зависимости, чтобы работать, поэтому он требует любого клиента, чтобы предоставить экземпляр через свой конструктор. Это гарантирует, что экземпляр доступен NeedyClass всякий раз, когда он необходим.

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

Как это работает

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

Листинг 4-1: Внедрение в конструктор
private readonly DiscountRepository repository;
public RepositoryBasketDiscountPolicy(
	DiscountRepository repository)
{
	if (repository == null)
	{
		throw new ArgumentNullException("repository");
	}
	this.repository = repository;
}

Строки 2-3: Внедрить зависимость как аргумент конструктора

Строки 5-8: Ограждающее условие (Guard Clause)

Строка 9: Сохранение зависимости для дальнейшего использования

Строка 1: Поле зависимости только для чтения

Зависимость (в предыдущем листинге это абстрактный класс DiscountRepository) является требуемым аргументом конструктора. Любой клиентский код, который не предоставляет экземпляра зависимости, не может скомпилироваться. Однако так как интерфейсы и абстрактные классы являются ссылочными типами, вызывающий элемент может передать null в качестве аргумента, чтобы вызывающий код мог быть скомпилирован, и мы должны защитить класс против таких злоупотреблений при помощи ограждающего условия (Guard Clause).

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

Хорошей практикой является то, чтобы отметить поле, содержащее зависимость, как readonly: это гарантирует, что как только инициализационная логика конструктора будет выполнена, поле не может быть изменено. Это не является обязательным с точки зрения DI, но это защитит вас от случайного изменения поля (например, установка его на null) где-то в другом месте зависимого от класса кода.

Примечание

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

Примечание

Подумайте о внедрении в конструктор как о статическом объявлении зависимостей класса. Сигнатура конструктора компилируется при помощи типа и доступна для всеобщего обозрения. Она четко говорит, что класс требует зависимости, которые он запрашивает через свой конструктор.

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

Когда это использовать

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

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

Совет

Если это вообще возможно, ограничивайтесь одним конструктором. Перегруженные конструкторы приводят к неоднозначности: какой конструктор должен использовать DI контейнер?

В случаях, когда локальная библиотека можем предоставить хорошую реализацию умолчанию, внедрение в свойство (Property Injection) может подойти лучше, но это нечастный случай. В предыдущих главах я показал много примеров хранилищ в качестве зависимостей. Это хорошие примеры зависимостей, когда локальная библиотека не может предоставить подходящую реализацию по умолчанию, так как надлежащая реализация принадлежит специализированным библиотекам доступа к данным (Data Access).

Таблица 4-1: Преимущества и недостатки внедрения в конструктор
Преимущества Недостатки
Внедрение точно есть Некоторые фреймворки усложняют использование внедрения в конструктор
Легко реализовать

Помимо гарантированного внедрения, о чем уже говорилось, этот паттерн также легко реализовать с помощью четырех шагов, показанных в листинге 4-1.

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

Очевидным недостатком внедрения в конструктор является то, что он требует, чтобы весь граф зависимости инициализировался немедленно, часто при запуске приложения. Однако, хотя это звучит не очень приятно, это редко является проблемой. В конце концов, даже для сложного графа объекта мы обычно говорим о создании десятков новых экземпляров объектов, а создание экземпляра объекта это то, что .NET Framework делает очень быстро. Любые узкие места, которые может иметь ваше приложение, появятся и в других местах, так что не беспокойтесь об этом.

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

Использование

Хотя внедрение в конструктор, как правило, повсеместно в приложениях, использующих DI, оно не очень часто присутствует в .NET Base Class Library (BCL). Главным образом это потому, что BCL представляет собой набор библиотек, а не полноценное приложение.

Два взаимосвязанных примера, когда мы видим своего рода внедрение конструктора в BCL, это с System.IO.StreamReader и System.IO.StreamWriter. Оба принимают в свои конструкторы экземпляр System.IO.Stream. У них также есть много перегруженных конструкторов, которые принимают путь к файлу, а не экземпляр Stream, но есть методы, которые внутренне создают FileStream на основе указанного пути к файлу: далее показаны все конструкторы StreamWriter, но конструкторы StreamReader схожи:

public StreamWriter(Stream stream);
public StreamWriter(string path);
public StreamWriter(Stream stream, Encoding encoding);
public StreamWriter(string path, bool append);
public StreamWriter(Stream stream, Encoding encoding, int bufferSize);
public StreamWriter(string path, bool append, Encoding encoding);
public StreamWriter(string path, bool append, Encoding encoding, int bufferSize);

Класс Stream является абстрактным классом, который служит в качестве абстракции, над которой работают StreamWriter и StreamReader для выполнения своих обязанностей. Вы можете указать любой реализацию Stream в их конструкторах, и они будут ее использовать, но они выбросят ArgumentNullExceptions, если вы попытаетесь присвоить Stream null.

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

Пример: Добавление выбора валюты для корзины покупок

Я хотел бы добавить новую функцию к примеру коммерческого приложения, который я представил в главе 2, а именно, способность выполнять конвертацию валют. Я расширю пример в данной главе, чтобы продемонстрировать различные DI паттерны в деле, и когда я все сделаю главная страница должно быть как на рисунке 4-4.

Рисунок 4-4: Пример коммерческого приложения с реализованной конвертацией валюты. Теперь пользователь может выбрать три разные валюта, и цена товара и общая стоимость (на странице корзины) будут отображаться в этой валюте.

Одна из первых вещей, которая вам нужна, это CurrencyProvider – зависимость, предоставляющая вам валюты, которые вы запрашиваете. Вы определяете это следующим образом:

public abstract class CurrencyProvider
{
	public abstract Currency GetCurrency(string currencyCode);
}

Класс Currency является другим абстрактным классом, который обеспечивает конверсию между собой и другими валютами:

public abstract class Currency
{
	public abstract string Code { get; }
	public abstract decimal GetExchangeRateFor(string currencyCode);
}

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

CurrencyProvider, скорее всего, представляют собой ресурс «вне процесса», например, веб-сервис или базу данных, которые предоставляют курсы конверсии. Это означает, что наиболее подходящим была бы реализация конкретного CurrencyProvider в отдельном проекте (например, в библиотеке Data Access). Следовательно, здесь нет никакой подходящей Local Default. В то же время, классу BasketController понадобится наличие CurrencyProvider; внедрение в конструктор – это то, что надо. Следующий листинг показывает, как зависимость CurrencyProvider внедряется в BasketController.

Листинг 4-2: Внедрение CurrencyProvider в BasketController
private readonly IBasketService basketService;
private readonly CurrencyProvider currencyProvider;
public BasketController(IBasketService basketService,
	CurrencyProvider currencyProvider)
{
	if (basketService == null)
	{
		throw new
			ArgumentNullException("basketService");
	}
	if (currencyProvider == null)
	{
		throw new
			ArgumentNullException("currencyProvider");
	}
	this.basketService = basketService;
	this.currencyProvider = currencyProvider;
}

Строки 3-4: Внедрить зависимость как аргумент конструктора

Строки 6-15: Ограждающее условие (Guard Clause)

Строки 16-17: Сохранение зависимости для дальнейшего использования

Строки 1-2: Поле зависимости только для чтения

Поскольку класс BasketController уже имеет зависимость в IBasketService, вы добавляете новую зависимость CurrencyProvider в качестве второго аргумента конструктора, а затем следуете той же последовательности, что изложена в листинге 4-1: ограждающее условие гарантирует, что зависимости не являются null, что означает, что хранить их для последующего использования в readonly полях безопасно.

Теперь, когда CurrencyProvider гарантированно присутствует в BasketController, он может быть использован в любом месте, например, в методе Index:

public ViewResult Index()
{
	var currencyCode = this.CurrencyProfileService.GetCurrencyCode();
	var currency = this.currencyProvider.GetCurrency(currencyCode);
	// …
}

Я еще не обсуждал CurrencyProfileService, так что теперь знайте, что он предоставляет предпочтительный код валюты текущего пользователя. Далее мы будем обсуждать CurrencyProfileService более подробно.

С имеющимся кодом валюты может быть вызван CurrencyProvider, чтобы предоставить Currency, который представляет этот код. Заметьте, что вы можете использовать поле currencyProvider без необходимости проверять его заранее, потому что оно гарантированно присутствует.

Теперь, когда у вас есть Currency, вы можете продолжить, чтобы выполнить остальную работу в методе Index; обратите внимание, что я еще не показал эту реализацию. По мере продвижения по этой главе я буду достраивать этот метод и попутно добавлю больше функционала по конвертации валюты.

Связанные паттерны

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

Если нам нужно сделать зависимость опциональной, мы можем перейти ко внедрению в свойство (Property Injection), если у нас есть подходящая Local Default.

Когда зависимость представляет Cross-Cutting Concern (CCC), который должен быть потенциально доступен для любого модуля в приложении, мы можем использовать Ambient Context.

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