Главная страница   /   5.1. Control Freak (Внедрение зависимостей в .NET

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

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

Марк Симан

5.1. Control Freak

Что является противоположностью инверсии управления? Первоначально термин инверсия управления был придуман, чтобы определить противоположность нормальному положению дел, но мы не можем на самом деле говорить об анти-паттерне «Business as Usual». Вместо этого, после долгих размышлений, я назвал его Control Freak, чтобы описать класс, который никак не желает терять контроль над своими зависимостями.

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

Совет

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

Анти-паттерн Control Freak появляется всякий раз, когда мы получаем экземпляр зависимости, прямо или косвенно используя ключевое слово new в любом месте, кроме Composition Root.

Примечание

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

Наиболее вопиющим примером Control Freak является то, когда мы не делаем никаких усилий, чтобы ввести абстракции в наш код. Вы видели несколько примеров в главе 2, когда Мэри реализовала свое коммерческое приложение (раздел 2.1.1). В таком подходе нет попытки ввести DI, но даже там, где разработчики слышали о DI и компоновке, анти-паттерн Control Freak часто может быть найден в различных вариациях.

В следующих разделах я покажу вам некоторые примеры, которые напоминают код, что я видел в производственном использовании. В любом случае, у разработчиков были лучшие намерения программирования, используя интерфейсы (programming to interfaces), но они никогда не понимали движущие силы и мотивацию.

Пример: обновление зависимостей

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

В главе 2, вы видели пример ProductService, который использует экземпляр абстрактного класса ProductRepository (листинг 2-6), чтобы получить список рекомендуемых товаров. В качестве напоминания, вот соответствующий метод по своей природе:

public IEnumerable<Product> GetFeaturedProducts(IPrincipal user)
{
	return from p in this.repository.GetFeaturedProducts()
	select p.ApplyDiscountFor(user);
}

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

Листинг 5-1: Обновление ProductRepository
private readonly ProductRepository repository;
public ProductService()
{
	string connectionString =
		ConfigurationManager.ConnectionStrings
		["CommerceObjectContext"].ConnectionString;
	this.repository = new SqlProductRepository(connectionString);
}

Строки 7: Напрямую создается новый экземпляр

Поле repository объявлено как абстрактный класс ProductRepository, поэтому любой член в классе ProductService (например, GetFeaturedProducts) будут разрабатываться при помощи интерфейса. Хотя это звучит правильно, но это не принесет особой пользы, потому что во время выполнения тип всегда будет SqlProductRepository. Нет никакой возможности перехватить или изменить переменную repository, пока вы не поменяете код и перекомпилируете его.

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

Обновление зависимостей напрямую при помощи new является лишь одним примером анти-паттерна Control Freak. Прежде чем я перейду к анализу и возможным путям исправления проблемы, созданной Control Freak, давайте посмотрим на несколько примеров, которые дадут вам более полное представление о контексте и общих неудачных попытках решения некоторых из полученных проблем.

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

Пример: фабрика

Наиболее распространенная (ошибочная) попытка исправить явные проблемы с обновлением зависимости при помощи new включает в себя какую-то фабрику. Есть несколько вариантов, где речь идет о фабриках, и я вкратце опишу каждый из них:

  • Конкретная фабрика (Concrete Factory)
  • Абстрактная фабрика (Abstract Factory)
  • Статическая фабрика (Static Factory)

Если сказать Мэри Роуэн (из главы 2), что она может иметь дело только с абстрактным классом ProductRepository, она введет ProductRepositoryFactory, которая будет создавать экземпляры, нужные для того, чтобы работа была сделана. Давайте послушаем, как она обсуждает такой подход со своим коллегой Йенсом: я думаю, что их обсуждение будет охватывать варианты фабрик, которые я перечислил:

Мэри: Нам нужен экземпляр ProductRepository в классе ProductService. Тем не менее, ProductRepository является абстрактным, поэтому мы не можем просто создать его новые экземпляры, и наш консультант говорит, что мы не должны также создавать новые экземпляры SqlProductRepository.

Йенс: Как насчет фабрики?

Мэри: Да, я подумала о том же, но я не знаю, что делать дальше. Я не понимаю, как она решит наши проблемы. Смотри ...

Мэри начинает писать код, чтобы показать проблему.

Конкретная фабрика

Это код, который пишет Мэри:

public class ProductRepositoryFactory
{
	public ProductRepository Create()
	{
		string connectionString =
			ConfigurationManager.ConnectionStrings
			["CommerceObjectContext"].ConnectionString;
		return new SqlProductRepository(connectionString);
	}
}

Мэри: Эта ProductRepositoryFactory инкапсулирует знания о том, как создать экземпляры ProductRepository, но это не решает проблемы, потому что нам нужно использовать это в ProductService вот так:

var factory = new ProductRepositoryFactory();
this.repository = factory.Create();

Мэри: Видишь? Теперь мы просто должны создать новый экземпляр класса ProductRepositoryFactory в ProductService, но это все же жестко кодирует использование SqlProductRepository. Единственное, чего мы добились, это переместили проблему в другой класс.

Йенс: Да, я вижу... А мы не можем решить проблему при помощи абстрактной фабрики?

Давайте поставим на паузу обсуждения Мэри и Йенса, чтобы оценить то, что произошло. Мэри совершенно права в том, что класс конкретной фабрики не решает проблему Control Freak, а только перемещает ее дальше. Это делает код более сложным. ProductService теперь непосредственно контролирует жизненный цикл фабрики, а фабрика непосредственно управляет жизненным циклом ProductRepository, поэтому мы до сих пор не может перехватить или заменить экземпляра repository во время выполнения.

Примечание

Не делайте выводов по этому разделу, что я вообще против использования классов конкретной фабрики. Конкретная фабрика может решить другие проблемы, такие как повторение кода, путем инкапсуляции сложной логики создания. Она просто не имеет никакой ценности для DI. Используйте ее, когда это имеет смысл.

Довольно очевидно, что конкретная фабрика не решит никаких проблем DI, и я думаю, что я никогда видел, чтобы она использовалась таким образом. Комментарий Йенса об абстрактной фабрике звучит более перспективно.

Абстрактная фабрика

Давайте продолжим дискуссию Мэри и Йенса и посмотрим, что Йенс может сказать об абстрактной фабрике.

Йенс: А что если мы сделаем фабрику абстрактной? Например, вот так:

public abstract class ProductRepositoryFactory
{
	public abstract ProductRepository Create();
}

Йенс: Это означает, что мы жестко не кодируем любые ссылки на SqlProductRepository, и мы можем использовать фабрику в ProductService, чтобы получить экземпляры ProductRepository.

Мэри: Но теперь, когда фабрика абстрактная, как мы получим ее новые экземпляры?

Йенс: мы создадим ее реализацию, которая возвращает экземпляры SqlProductService.

Мэри: Да, но как мы создадим экземпляр этого?

Йенс: мы просто используем ключевое слово new в ProductService... Ой, подожди...

Мэри: Это просто вернет нас назад, откуда мы начали.

Мэри и Йенс быстро поняли, что абстрактная фабрика не меняет ситуацию. Суть заключается в том, что им нужен был экземпляр абстрактного класса ProductRepository, а теперь вместо этого нужен экземпляр абстрактной ProductRepositoryFactory.

Абстрактная фабрика

Абстрактная фабрика является одним из шаблонов проектирования из оригинальной книги Design Patterns. Она полезна для DI, потому что она может инкапсулировать сложную логику, которая создает другие зависимости.

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

Паттерн Abstract Factory встречается гораздо чаще, нежели вы можете ожидать: имена включенных классов часто скрывают этот факт. Класс CurrencyProvider представленный в разделе 4.1.4, на самом деле является абстрактной фабрикой с другим именем: это абстрактный класс, который создает экземпляры другого абстрактного класса (Currency).

В разделе 6.1 мы вернемся к паттерну Abstract Factory, чтобы увидеть, как он может помочь в решении одного типа проблем, которые часто происходят с DI.

Мэри и Йенс сделали только такую реализацию фабрики, которая не была бы вредна для них. С другой стороны, это также не решит их проблемы; а поскольку предполагается, что логика создания для экземпляров ProductRepository не будет сложной, такое использование не имеет никакой ценности.

Теперь, когда Мэри и Йенс отвергли единственную безопасную реализацию фабрики, только один вариант остается открытым.

Статическая фабрика

Мэри и Йенс собираются прийти к выводу. Давайте послушаем, как они принимают решения о подходе, который, как они думают, будет работать:

Мэри: Давай сделаем статическую фабрику. Я тебе покажу:

public static class ProductRepositoryFactory
{
	public static ProductRepository Create()
	{
		string connectionString =
			ConfigurationManager.ConnectionStrings
			["CommerceObjectContext"].ConnectionString;
		return new SqlProductRepository(connectionString);
	}
}

Мэри: Теперь, когда класс статический, нам не нужно думать, как его создать.

Йенс: Но у нас по-прежнему остается жесткая закодированность в том, что мы возвращаем экземпляры SqlProductRepository, поэтому разве это нам как-то поможет?

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

public static ProductRepository Create()
{
	var repositoryType =
		ConfigurationManager.AppSettings["productRepository"];
	switch (repositoryType)
	{
		case "sql":
			return ProductRepositoryFactory.CreateSql();
		case "azure":
			return ProductRepositoryFactory.CreateAzure();
		default:
			throw new InvalidOperationException("...");
	}
}

Мэри: Видишь? Таким образом, мы можем определить, должны ли мы использовать реализацию на основе SQL Server или реализацию на основе Windows Azure, и нам даже не нужно перекомпилировать приложение, чтобы переходить от одной к другой.

Йенс: Супер! Это то, что мы сделаем. Консультант должен быть счастлив.

Есть несколько причин, почему такая статическая фабрика не дает удовлетворительного решения первоначальной цели программировании при помощи интерфейсов. Давайте посмотрим на граф зависимостей на рисунке 5-2.

Рисунок 5-2: Граф зависимостей для предполагаемого решения: статический ProductRepositoryFactory используется для создания экземпляров ProductRepository.

Я не приукрашиваю

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

Это был довольно большой проект, который был нацелен на центральную деловую область компании Fortune 500, поэтому надлежащая модульность была важна в связи со сложностью применения. К сожалению, я стал участвовать в проекте слишком поздно, и мои предложения были отклонены, потому что они несли драматические изменения в уже разработанный код.

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

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

Все классы должны ссылаться на абстрактный класс ProductRepository:

  • ProductService, потому что потребляет экземпляры ProductRepository
  • ProductRepositoryFactory, потому что раскрывает экземпляры ProductRepository
  • AzureProductRepository и SqlProductRepository, потому что они реализуют ProductRepository

ProductRepositoryFactory зависит от обоих классов AzureProductRepository и SqlProductRepository. Поскольку ProductService напрямую зависит от ProductRepositoryFactory, он также зависит от обеих реализаций ProductRepository.

Крах зависимости

Плохая ситуация возникает, когда абстракция ProductRepository и потребляющий ProductService определены в одной сборке (как это имеет место с реализациями, которые я создавал в книге до сих пор). Давайте предположим, что это сборка доменной модели. В этом случае, ProductRepositoryFactory также должна быть в этой же сборке, иначе у нас была бы циклическая ссылка, которая не представляется возможной.

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

Когда AzureProductRepository и SqlProductRepository реализованы в сборке доменной модели, это полностью идет вразрез с принципом разделения понятий (Separation of Concerns). Мы, по существу, останемся с монолитным приложением.

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

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

Если Мэри и Йенсу когда-нибудь понадобится третий тип ProductRepository, им придется изменить фабрику и перекомпилировать решение. Хотя их решение может быть настраиваемым, оно не является расширяемым.

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

Примечание

Динамические mock выходят за рамки этой книги, но я кратко коснулся данной темы, когда я описывал тестируемость в главе 1 (раздел 1.2.2).

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

Теперь, когда вы увидели много примеров Control Freak, я надеюсь, у вас есть довольно хорошее представление о том, что искать: вхождения ключевого слова new рядом с зависимостями. Это может позволить вам избежать наиболее очевидных ловушек, но если вам нужно оградить себя от возникновения этого анти-паттерна, в следующем разделе обсуждается, как бороться с такой проблемой.

Анализ

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

Влияние

С тесно связанным кодом, который является результатом Control Freak, теряются многие преимущества модульной конструкции:

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

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

Рефакторинг к DI

Чтобы избавиться от Control Freak, мы должны сделать рефакторинг нашего кода к одному из паттернов проектирования DI, представленных в главе 4. В качестве первого шага, мы должны использовать руководство, чтобы определить, какой паттерн выбрать. В большинстве случаев это будет внедрение в конструктор. Шаги рефакторинга заключаются в следующем:

  1. Убедитесь, что вы программируете, используя интерфейсы. В примерах, которые я только что представил, это уже имело место, но в других ситуациях, возможно, потребуется сначала извлечь интерфейс и изменить объявления переменных.
  2. Если вы создаете конкретную реализацию зависимостей в нескольких местах, переместите их все в один метода создания. Убедитесь, что возвращаемое значение этого метода выражается в виде абстракции, а не конкретного типа.
  3. Теперь, когда у вас есть только одно место, где вы создаете экземпляр, переместите это создание из потребляющего класса путем реализации одного из DI паттернов, такого как внедрение в конструктор.

В случае с примерами ProductService из предыдущих разделов, внедрение в конструктор является отличным решением:

private readonly ProductRepository repository;
public ProductService(ProductRepository repository)
{
	if (repository == null)
	{
		throw new ArgumentNullException("repository");
	}
	this.repository = repository;
}

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

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

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