我有一个类型Connections
,需要异步初始化。此类型的实例被几个其他类型(例如Storage
)所使用,每个类型也需要异步初始化(静态的,不是针对每个实例的,并且这些初始化也依赖于Connections
)。最后,我的逻辑类型(例如Logic
)消耗这些存储实例。目前使用Simple Injector。
我尝试了几种不同的解决方案,但总是存在反模式。
显式初始化(时间耦合)
我当前正在使用的解决方案具有时间耦合反模式:
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);
}
}
我已经将时间耦合封装到一个方法中,所以情况不会像可能的那么糟糕。但是,它仍然是反模式,并且不像我想要的那样可维护。
抽象工厂(同步-异步)
一个常见的解决方案是抽象工厂模式。然而,在这种情况下,我们处理异步初始化。所以,我可以通过强制使初始化同步运行来使用抽象工厂模式,但这会采用同步覆盖异步的反模式。我真的不喜欢同步覆盖异步的方法,因为我有几个存储器,在我的当前代码中,它们都在并发初始化;由于这是云应用程序,将其更改为串行同步将增加启动时间,并行同步也不理想,因为会消耗资源。
异步抽象工厂(不当的抽象工厂用法)
我也可以使用具有异步工厂方法的抽象工厂。然而,这种方法有一个主要问题。正如Mark Seeman在这里评论的那样,“任何值得一试的DI容器都能够正确地为您自动连接一个[factory]实例”,但是很遗憾,对于异步工厂,这完全不成立。据我所知,没有DI容器支持此功能。
因此,抽象异步工厂解决方案需要我使用显式工厂,至少是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);
}
这会带来几个问题:
- 我所有的工厂注册都必须从容器中显式提取依赖项并将它们传递给
CreateAsync
。因此 DI 容器不再执行依赖注入。 - 这些工厂调用的结果具有不再由 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 容器在“简单构造函数”的方式上采取强硬立场,那么他们只是强迫用户在其他地方进行自己的初始化,这会带来自己的反模式。例如,为什么 Simple Injector 不考虑异步功能:“不,这个功能对于 Simple Injector 或任何其他 DI 容器都没有意义,因为它违反了关于依赖注入的一些重要基本规则。”然而,严格遵守“基本规则”显然会导致其他更糟糕的反模式。
问题是:是否有一种解决异步初始化的方法,可以避免所有反模式?
更新:上面提到的 Connections
的完整签名:
public sealed class AzureConnections
{
public AzureConnections();
public CloudStorageAccount CloudStorageAccount { get; }
public CloudBlobClient CloudBlobClient { get; }
public CloudTableClient CloudTableClient { get; }
public async Task InitializeAsync();
}
AzureConnections
视为第三方依赖项,并将其封装在一个抽象层后面,其实现将处理调用异步初始化(时间)代码的操作。 - Nkosinew Logic(await Storage.CreateAsync(await Connections.CreateAsync()));
这样构建。当然,自己操作时会失去DI的“注入”部分,这会成为维护负担。 - Stephen Cleary