У меня есть тип Connections
, который требует асинхронной инициализации. Экземпляр этого типа потребляется несколькими другими типами (например, Storage
), каждый из которых также требует асинхронной инициализации (статический, а не для каждого экземпляра, и эти инициализации также зависят от Connections
). Наконец, мои логические типы (например, Logic
) потребляют эти экземпляры хранилища. В настоящее время используется простой инжектор.
Я пробовал несколько разных решений, но всегда присутствует антипаттерн.
Явная инициализация (временная связь)
В решении, которое я использую в настоящее время, есть антипаттерн Temporal Coupling:
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);
}
}
Я инкапсулировал временную связь в метод, так что это не так плохо, как могло бы быть. Но тем не менее, это антипаттерн, а не такой удобный, как мне хотелось бы.
Аннотация Factory (Sync-Over-Async)
Общим предлагаемым решением является абстрактный шаблон Factory. Однако в этом случае мы имеем дело с асинхронной инициализацией. Таким образом, я мог бы использовать Abstract Factory, заставляя инициализацию работать синхронно, но затем принимает antipattern sync-over-async. Мне очень не нравится подход sync-over-async, потому что у меня есть несколько хранилищ, и в моем текущем коде они все инициализируются одновременно; поскольку это облачное приложение, изменение этого для последовательного синхронного будет увеличивать время запуска, а параллельная синхронность также не идеальна из-за потребления ресурсов.
Асинхронный абстрактный Factory (неправильное использование Factory)
Я также могу использовать Abstract Factory с асинхронными методами Factory. Однако есть одна серьезная проблема с этим подходом. Как замечает Марк Симан здесь, "любой контейнер DI, достойный его соли, сможет автоматически установить для вас экземпляр [ factory], если вы зарегистрируете его правильно". К сожалению, это совершенно неверно для асинхронных фабрик: AFAIK не поддерживает контейнер DI, который поддерживает это.
Итак, решение Abstract Asynchronous Factory потребовало бы, чтобы я использовал явные фабрики, по крайней мере Func<Task<T>>
, и это заканчивается повсюду ( "Мы лично считаем, что разрешить регистрацию делегатов Func по умолчанию - это запах дизайна... Если у вас много конструкторов в вашей системе, которые зависят от 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);
}
Это вызывает несколько собственных проблем:
- Все мои учетные записи Factory должны явно выгружать зависимости из контейнера и передавать их на
CreateAsync
. Таким образом, контейнер DI больше не работает, вы знаете, инъекции зависимостей. - Результаты этих вызовов Factory имеют срок службы, которые больше не управляются контейнером DI. Каждый Factory теперь отвечает за управление жизненным циклом, а не за контейнер DI. (С синхронным тестом Factory это не проблема, если Factory зарегистрирован соответствующим образом).
- Любой метод, использующий эти зависимости, должен быть асинхронным - поскольку даже логические методы должны ждать завершения инициализации хранилища/соединений. Это не очень важно для меня в этом приложении, так как мои методы хранения все равно асинхронны, но это может быть проблемой в общем случае.
Самоинициализация (временная связь)
Другое, менее распространенное решение состоит в том, чтобы каждый член типа ожидал своей собственной инициализации:
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();
}
}
Проблема в том, что мы вернулись к Temporal Coupling, на этот раз разбросанные по всей системе. Кроме того, для этого подхода все публичные члены должны быть асинхронными методами.
Итак, на самом деле есть две перспективы дизайна DI, которые здесь расходятся:
- Потребители хотят иметь возможность вводить экземпляры, которые готовы к использованию.
- Контейнеры DI нажимают для простых конструкторов.
Проблема - особенно при асинхронной инициализации - что если контейнеры DI принимают жесткую линию подхода "простых конструкторов", то они просто заставляют пользователей выполнять свою собственную инициализацию в другом месте, что приводит к собственным антипаттернам. Например, почему Simple Injector не будет рассматривать асинхронные функции: "Нет, такая функция не имеет смысла для Simple Injector или любого другого контейнера DI, потому что он нарушает несколько важных основополагающих правил, когда дело доходит до инъекции зависимости". Тем не менее, игра строго "по основным правилам", по-видимому, заставляет другие антипаттерны казаться намного хуже.
Вопрос: существует ли решение для асинхронной инициализации, которое позволяет избежать всех антипаттернов?
Обновление: полная подпись для AzureConnections
(упомянутая выше как Connections
):
public sealed class AzureConnections
{
public AzureConnections();
public CloudStorageAccount CloudStorageAccount { get; }
public CloudBlobClient CloudBlobClient { get; }
public CloudTableClient CloudTableClient { get; }
public async Task InitializeAsync();
}