Преобразование динамических значений в абстракции

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

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

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

Абстракции с зависимостями времени выполнения

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

Рисунок 6-1a:

Это никогда не будет работать, если во время выполнения неясно, какая реализация DiscountRepository должна быть использована. Во время разработки мы можем рассматривать зависимость как абстракцию и следовать Принципу подстановки Барбары Лисков (Liskov Substitution Principle), но во время выполнения решение о том, какой DiscountRepository использовать, должно быть принято до того, как будет создан RepositoryBasketDiscountPolicy. Поскольку зависимость запрашивается через конструктор, мы не можем принять решения после этого момента.

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

Примечание

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

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

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

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

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

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

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

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

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

public interface IFooFactory
{
	IFoo Create(Bar bar);
}

В ухудшенном варианте абстрактная фабрика может не принимать никаких входных параметров:

public interface IFooFactory
{
	IFoo Create();
}

В таком случае абстрактная фабрика становится чистой фабрикой, в то время как аспект преобразования исчезает.

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

Совет

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

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

Проектные требования

Насколько полезной может быть абстрактная фабрика, настолько осторожно мы должны применять ее. Зависимости, созданные абстрактной фабрикой, должны концептуально требовать значению времени выполнения. Переход от значения времени выполнения в абстракцию должен иметь смысл на концептуальном уровне. Если вы чувствуете желание ввести абстрактную фабрику, чтобы иметь возможность создавать экземпляры конкретной реализации, стоит воспользоваться протекающей абстракцией (Leaky Abstraction).

Протекающие абстракции

Так же, как разработка через тестирование (Test-Driven Development, TDD) обеспечивает тестируемость, безопаснее сначала определить интерфейсы, а затем дальнейшую программу для них. Тем не менее, бывают случаи, когда у нас уже есть конкретный тип и теперь мы хотим извлечь интерфейс.

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

Если нам нужно извлечь интерфейс, мы должны делать это рекурсивном образом, гарантируя, что все типы, извлекаемые корневым интерфейсом, сами являются интерфейсами. Я называю это глубоким извлечением (Deep Extraction), а результат – глубокими интерфейсами (Deep Interfaces).

В ASP.NET MVC есть некоторые примеры извлечения глубоких интерфейсов. Например, у HttpContextBase есть свойство Request типа HttpRequestBase, и так далее. Эта абстракция была рекурсивно извлечена из System.Web.HttpContext.

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

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

Примечание

Любая абстракция, которая создает экземпляры других абстракций, является фабрикой абстракций (абстрактной фабрикой). И для этого не нужно иметь имя, которое заканчивается на Factory.

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

Пример: выбор алгоритма маршрута

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

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

public enum RouteType
{
	Shortest = 0,
	Fastest,
	Scenic
}

Однако то, что вам нужно, это экземпляр IRouteAlgorithm, который может рассчитать маршрут для вас. Для перехода от значения времени выполнения RouteType к IRouteAlgorithm, вы можете определить абстрактную фабрику:

public interface IRouteAlgorithmFactory
{
	IRouteAlgorithm CreateAlgorithm(RouteType routeType);
}

Это позволяет реализовать метод GetRoute для RouteController путем внедрения IRouteAlgorithmFactory и использовать это для перевода значения времени выполнения в нужную зависимость: IRouteAlgorithm. Следующий листинг демонстрирует взаимодействие.

Листинг 6-1: Использование IRouteAlgorithmFactory
public class RouteController
{
	private readonly IRouteAlgorithmFactory factory;
	public RouteController(IRouteAlgorithmFactory factory)
	{
		if (factory == null)
		{
			throw new ArgumentNullException("factory");
		}
		this.factory = factory;
	}
	public IRoute GetRoute(RouteSpecification spec,
		RouteType routeType)
	{
		IRouteAlgorithm algorithm =
			this.factory.CreateAlgorithm(routeType);
		return algorithm.CalculateRoute(spec);
	}
}

Строки 15-16: Преобразование значения времени выполнения

Строка 17: Использовать преобразованный алгоритм

Ответственность класса RouteController заключается в обработке веб запросов. Метод GetRoute получает спецификацию пользователя о пунктах отправления и назначения, а также выбранный тип маршрута (через RouteType). Вам нужна абстрактная фабрика для преобразования значения времени выполнения RouteType в экземпляр IRouteAlgorithm, поэтому вы запрашиваете экземпляр IRouteAlgorithmFactory, используя стандартное внедрение в конструктор.

В методе GetRoute вы можете использовать factory для преобразования переменной routeType в IRouteAlgorithm. Когда это будет сделано, вы можете использовать это для расчета маршрута и возвращения результата.

Примечание

Для краткости я опустил ограждающее условие в методе GetRoute. Тем не менее, предоставляемый RouteSpecification может быть null, поэтому в более совершенной реализации нужно сделать на это проверку.

Наиболее очевидная реализация IRouteAlgorithmFactory будет включать простой оператор switch и возвращать три различные реализации IRouteAlgorithm на основе входных данных. Тем не менее, я оставляю это как упражнение для читателя.

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

Пример: использование CurrencyProvider

В большей части главы 4 вы видели, как реализовать конвертацию валюты в контроллере ASP.NET MVC. Тип Currency является абстрактным классом, воспроизведенным здесь, чтобы вам не пришлось возвращаться в раздел 4.1.4:

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

На первый взгляд, кажется немного странным, обрабатывать такое понятие, как валюта, в качестве абстракции, потому что оно звучит скорее как Объект-значение (Value Object). Тем не менее, обратите внимание, что метод GetExchangeRateFor позволяет нам запросить его для практически неограниченного множества конверсий. Предположим, есть 100 курсов обмена, каждый экземпляр Currency будет потреблять больше, чем 2 КБ памяти. Это вроде бы и не так много, но, возможно, потребуется оптимизация, например, использование паттерна проектирования Приспособленец (Flyweight).

Другой вопрос, который сразу возникает при конвертации валюты, касается денег (sic!) валюты: другими словами, ее актуальности. Такие приложения, как трейдерское программное обеспечение для монетарных рынков, требуют того, чтобы курсы валют обновлялись несколько раз в секунду, в то время как международные коммерческие сайты, скорее всего, обойдутся несколькими обновлениями для стабильной валюты. Такие приложения могут также включать разметку или стратегии округления, добавляя потенциальную сложность в реализацию типа Currency. В свете этого, абстрактный класс Currency кажется вполне разумным.

Когда потребителю, например, ASP.NET MVC контроллеру, необходимо преобразовать цены, он затребует Currency как зависимость для выполнения конвертации. В примере коммерческого приложения, используемого в этой книге, у класса Money, который используются для представления цен, есть этот метод конверсии:

public Money ConvertTo(Currency currency)

Потребители, такие как контроллер, могут предоставить экземпляр Currency всем ценам, чтобы конвертировать их, но теперь возникает вопрос, какой экземпляр Currency?

Выбор целевого Currency зависит от значения времени выполнения: выбранной пользователем валюты. Это означает, что мы не можем запросить один объект Currency через внедрение в конструктор, потому что Composer не сможет узнать, какую валюту использовать.

Как вы видели в разделе 4.1.1, решением является внедрение CurrencyProvider вместо одного Currency:

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

Рисунок 6-2 показывает, как контроллер обычно извлекает код предпочтительной валюты пользователя из профиля и использует внедренный CurrencyProvider для создания соответствующего экземпляра Currency.

Рисунок 6-2: Внедренный CurrencyProvider используется для отображения простого значения времени выполнения (строку кода валюты) в зависимость времени выполнения (экземпляр Currency).

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

Еще один пример из главы 4 показывает дегенеративный случай, где нет никаких первоначальных входных параметров. В разделе 4.2.4 вы видели, как у абстрактного CurrencyProfileService есть метод GetCurrencyCode, который будет возвращать текущий валютный код пользователя:

public abstract string GetCurrencyCode();

Хотя метод GetCurrencyCode возвращает строку, а не абстракцию, вы можете рассматривать CurrencyProfileService как вариант абстрактной фабрики.

В HomeController вы объединяете оба варианта, чтобы выяснить предпочтительную валюту пользователя:

var currencyCode = this.CurrencyProfileService.GetCurrencyCode();
var currency = this.currencyProvider.GetCurrency(currencyCode);

И в CurrencyProfileService, и в currencyProvider внедряются абстрактные фабрики, которые доступны для любого члена класса HomeController. В разделах 4.1.4 и 4.2.4 показано, как они внедряются.

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

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

или RSS канал: Что новенького на smarly.net