Исключение всех антипаттернов DI для типов, требующих асинхронной инициализации
у меня есть типа Connections
это требует асинхронной инициализации. Экземпляр этого типа используется несколькими другими типами (например,Storage
), каждая из которых также требует асинхронной инициализации (статической, а не для каждого экземпляра, и эти инициализации также зависят от Connections
). Наконец, мои логические типы (например,Logic
) потребляет эти экземпляры для хранения. В настоящее время используется простой инжектор.
я пробовал несколько разных решений, но всегда есть причина подарок.
Явная Инициализация (Временная Связь)
решение, которое я сейчас использую, имеет антипаттерн временной связи:
public sealed class Connections
{
Task InitializeAsync();
}
public sealed class Storage : IStorage
{
public Storage(Connections connections);
public static Task InitializeAsync(Connections connections);
}
public sealed class Logic
{
public Logic(IStorage storage);
}
public static class GlobalConfig
{
public static async Task EnsureInitialized()
{
var connections = Container.GetInstance<Connections>();
await connections.InitializeAsync();
await Storage.InitializeAsync(connections);
}
}
я инкапсулировал временную связь в метод, так что это не так плохо, как могло бы быть. Но все же, это антипаттерн и не такой доступный, как мне хотелось бы.
Абстрактная Фабрика (Sync-Over-Async)
общим предлагаемым решением является абстрактная Фабрика узор. Однако в данном случае мы имеем дело с асинхронной инициализации. Итак, Я мог бы используйте абстрактную фабрику, заставляя инициализацию работать синхронно, но затем она принимает антипаттерн sync-over-async. Мне очень не нравится подход sync-over-async, потому что у меня есть несколько хранилищ, и в моем текущем коде все они инициализируются одновременно; поскольку это облачное приложение, изменение этого на последовательно Синхронное увеличит время запуска и параллельное синхронный также не идеален из-за потребления ресурсов.
Асинхронная Абстрактная Фабрика (Неправильное Использование Абстрактной Фабрики)
я также могу использовать абстрактную фабрику с асинхронными заводскими методами. Однако в этом подходе есть одна серьезная проблема. Как Марк Seeman комментарии здесь, " любой контейнер DI стоит своей соли сможет автоматически подключить экземпляр [фабрики] для вас, если вы зарегистрируете его правильно."К сожалению, это полностью неверно для асинхронных фабрик: AFAIK есть нет DI контейнер, который поддерживает это.
Итак, абстрактное асинхронное заводское решение потребует от меня использования явных фабрик, по крайней мере Func<Task<T>>
и это кончится везде ("мы лично считаем, что разрешение регистрировать делегатов Func по умолчанию является дизайнерским запахом... Если у вас есть много конструкторов в вашей системе, которые зависят от функций, пожалуйста, внимательно посмотрите на свои стратегия зависимости."):
public sealed class Connections
{
private Connections();
public static Task<Connections> CreateAsync();
}
public sealed class Storage : IStorage
{
// Use static Lazy internally for my own static initialization
public static Task<Storage> CreateAsync(Func<Task<Connections>> connections);
}
public sealed class Logic
{
public Logic(Func<Task<IStorage>> storage);
}
это вызывает несколько собственных проблем:
- все мои заводские регистрации должны явно извлекать зависимости из контейнера и передавать их в
CreateAsync
. Таким образом, контейнер DI больше не делает, вы знаете,инъекции зависимостей. - результаты этих заводских вызовов имеют время жизни, которое больше не управляется контейнером DI. Каждое предприятие отвечает за управление жизненным циклом вместо контейнера. (С синхронной абстрактной фабрикой это не проблема, если фабрика зарегистрирована соответствующим образом).
- любой метод, фактически использующий эти зависимости, должен быть асинхронным, поскольку даже логические методы должны ожидать завершения инициализации хранилища/соединений. Это не имеет большого значения для меня в этом приложении, так как мои методы хранения все равно асинхронны, но это может быть проблемой в целом случай.
Самоинициализация (Временная Связь)
другое, менее распространенное решение состоит в том, чтобы каждый член типа ждал своей собственной инициализации:
public sealed class Connections
{
private Task InitializeAsync(); // Use Lazy internally
// Used to be a property BobConnection
public X GetBobConnectionAsync()
{
await InitializeAsync();
return BobConnection;
}
}
public sealed class Storage : IStorage
{
public Storage(Connections connections);
private static Task InitializeAsync(Connections connections); // Use Lazy internally
public async Task<Y> IStorage.GetAsync()
{
await InitializeAsync(_connections);
var connection = await _connections.GetBobConnectionAsync();
return await connection.GetYAsync();
}
}
public sealed class Logic
{
public Logic(IStorage storage);
public async Task<Y> GetAsync()
{
return await _storage.GetAsync();
}
}
все открытые члены должны быть асинхронными методами.
Итак, есть действительно две перспективы дизайна DI что здесь не сходится:
- потребители хотят иметь возможность вводить экземпляры, которые готовы к использованию.
- контейнеры DI нажимают крепко для простые конструкторы.
проблема - особенно с асинхронной инициализацией - в том, что если контейнеры DI занимают жесткую линию на подходе "простых конструкторов", то они просто заставляют пользователей выполнять свою собственную инициализацию в другом месте, что приносит свои собственные антипаттеры. Например., почему простой инжектор не будет рассматривать асинхронные функции: "нет, такая функция не имеет смысла для простого инжектора или любого другого контейнера DI, потому что она нарушает несколько важных основных правил, когда дело доходит до инъекции зависимостей. Тем не менее, игра строго "по основным правилам", по-видимому, заставляет других антипаттеров казаться намного хуже.
вопрос: есть ли решение для асинхронной инициализации, чтобы избежать всех антимоделей?
Update: полная подпись для AzureConnections
(упомянутый выше как Connections
):
public sealed class AzureConnections
{
public AzureConnections();
public CloudStorageAccount CloudStorageAccount { get; }
public CloudBlobClient CloudBlobClient { get; }
public CloudTableClient CloudTableClient { get; }
public async Task InitializeAsync();
}
2 ответов
проблема у вас есть, и приложение, которое вы строите, это-типично. Это типично по двум причинам:--14-->
- вам нужна (или, скорее, хотите) асинхронная инициализация запуска и
- ваша платформа приложений (функции azure) поддерживает асинхронную инициализацию запуска (или, скорее, вокруг нее мало рамок). Это делает вашу ситуацию немного отличной от обычного сценария, который может сделать ее немного труднее обсуждать общие закономерности.
однако, даже в вашем случае решение достаточно простое и элегантное:
извлеките инициализацию из классов, которые ее содержат, и переместите инициализацию в корень композиции. В этот момент Вы можете создать и инициализировать эти классы до регистрация их в контейнере и подача этих инициализированных классов в контейнер как часть регистраций.
этот хорошо работает в вашем конкретном случае, потому что вы хотите сделать некоторую (одноразовую) инициализацию запуска. Инициализация запуска обычно выполняется перед настройкой контейнера, а иногда и после, если для этого требуется полностью составленный граф объектов. В большинстве случаев, которые я видел, инициализация может быть выполнена раньше, как и в вашем случае.
- инициализация запуска синхронный. Рамки (например ASP.NET Core) обычно не поддерживают асинхронную инициализацию на этапе запуска
- инициализация часто должна выполняться по запросу, точно в срок, а не по заявке, заранее. Часто компоненты, которые нуждаются в инициализации, имеют короткий срок службы, что означает, что мы обычно инициализируем такой экземпляр при первом использовании (другими словами: just-in-time).
типично, никакое реальное преимущество делать запуск инициализация асинхронного. Нет никакого практического преимущества в производительности, так как во время запуска все равно будет работать только один поток (хотя мы можем распараллелить его, но это, очевидно, не требует асинхронности). Также обратите внимание, что, хотя некоторые типы приложений могут взаимоблокироваться при выполнении синхронизации по асинхронности, в корне композиции Мы знаем ровно какой тип приложения мы используем и будет ли это проблемой или нет. Корень композиции конкретные приложения. Другими словами, когда у нас есть инициализация в нашем корне композиции, обычно нет преимущества асинхронной инициализации запуска.
поскольку в корне композиции Мы знаем, является ли проблема синхронизации или нет, мы можем даже решить выполнить инициализацию при первом использовании и синхронно. Поскольку количество инициализации конечное (по сравнению с инициализацией по запросу), нет практического влияния на производительность фоновый поток с синхронной блокировкой, если мы хотим. Все, что нам нужно сделать, это определить прокси-класс В нашем корне композиции, который гарантирует, что инициализация выполняется при первом использовании. Это в значительной степени идея, которую Марк Seemann предложил в качестве ответа.
Я вообще не был знаком с функциями Azure, поэтому на самом деле это первый тип приложения (кроме консольных приложений, конечно), который я знаю, который фактически поддерживает асинхронную инициализацию. В большинстве типов framework нет способа для пользователей, чтобы сделать эту инициализацию запуска асинхронно вообще. Когда мы внутри Application_Start
событие в ASP.NET приложение или в классе Startup ASP.NET например, в основном приложении нет асинхронности. Все должно быть синхронно.
кроме того, фреймворки приложений не позволяют нам создавать их корневые компоненты фреймворка асинхронно. Даже если контейнеры DI будут поддерживать концепцию выполнения асинхронных разрешений, это не сработает из-за "отсутствие" поддержки фреймворков приложений. Взять ASP.NET ядро IControllerActivator
например. Его Create(ControllerContext)
метод позволяет создать экземпляр контроллера, но возвращаемый тип object
, а не Task<object>
. Другими словами, даже если контейнеры DI предоставят нам ResolveAsync
метод, это все равно вызовет блокировку, потому что ResolveAsync
вызовы будут обернуты за синхронными абстракциями фреймворка.
в большинстве случаев, вы увидите, что инициализация выполняется для каждого экземпляра, или в во время выполнения. Например, SqlConnections обычно открываются по запросу, поэтому каждый запрос должен открыть свое собственное соединение. Когда мы хотим открыть соединение "как раз вовремя", это неизбежно приводит к асинхронным интерфейсам приложений. Но будьте осторожны здесь:
если мы создаем синхронную реализацию, мы должны сделать ее абстракцию синхронной только в том случае, если мы уверены, что никогда не будет другой реализации (или прокси, декоратор, перехватчик, и т. д.), Что является асинхронным. Если мы недействительно делаем абстракцию синхронной (т. е. имеем методы и свойства, которые не выставляют Task<T>
), у нас вполне может быть Дырявая Абстракция в наши руки. Это может привести к радикальным изменениям во всем приложении, когда мы получим асинхронную реализацию позже.
другими словами, с введением async мы должны еще больше заботиться о дизайне наших абстракций приложений. Этот это касается и вашего дела. Даже если вам может потребоваться только инициализация запуска, вы уверены ,что для абстракций, которые вы определили (и AzureConnections
также), вам никогда не понадобится асинхронная инициализация just-in-time? В случае синхронного поведения AzureConnections
- это деталь реализации, вам нужно будет сделать ее асинхронной сразу.
Другим примером этого является ваш INugetRepository. Его члены синхронны, но это явно дырявая абстракция, поскольку причина его синхронности заключается в том, что его реализация является синхронной. Однако его реализация является синхронной, поскольку он использует устаревший пакет NuGet NuGet, который имеет только синхронный API. Совершенно ясно, что INugetRepository
должен быть полностью асинхронным, даже если его реализация является синхронной.
в приложении, применяющем async, большинство абстракций приложения будут иметь в основном асинхронные члены. Когда это так, было бы нетрудно сделать это своего рода асинхронная логика инициализации just-in-time; все уже асинхронно.
подведем итоги:
- в случае, если вам нужна инициализация запуска: сделайте это до или после настройки контейнера или после. Это делает составление графов объектов быстрым, надежным и проверяемым.
- выполнение инициализации перед настройкой контейнера предотвращает временное соединение, но может означать, что вам придется переместить инициализацию из классов, которые требуют это (что на самом деле хорошо).
- в большинстве типов приложений асинхронная инициализация запуска невозможна, в других типах она обычно не нужна.
- в случае, если вам требуется инициализация per-request или just-in-time, нет никакого способа обойти асинхронные интерфейсы.
- будьте осторожны с синхронными интерфейсами если вы создаете асинхронное приложение, вы можете утечка деталей реализации.
хотя я уверен, что следующее Не то, что вы ищете, можете ли вы объяснить, почему он не отвечает на ваш вопрос?
public sealed class AzureConnections
{
private readonly Task<CloudStorageAccount> storage;
public AzureConnections()
{
this.storage = Task.Factory.StartNew(InitializeStorageAccount);
// Repeat for other cloud
}
private static CloudStorageAccount InitializeStorageAccount()
{
// Do any required initialization here...
return new CloudStorageAccount( /* Constructor arguments... */ );
}
public CloudStorageAccount CloudStorageAccount
{
get { return this.storage.Result; }
}
}
чтобы сохранить дизайн ясным, я реализовал только одно из свойств облака, но два других могут быть выполнены аналогичным образом.
на AzureConnections
конструктор не будет блокировать, даже если для инициализации различных облачных объектов потребуется значительное время.
Он, с другой стороны, начнет работу, и поскольку задачи .NET ведут себя как обещания, при первой попытке получить доступ к значению (используя Result
) он собирается вернуть значение, произведенное InitializeStorageAccount
.