Ste*_*ary 29 dependency-injection initialization abstract-factory async-await simple-injector
我有一个Connections需要异步初始化的类型.这种类型的实例被其他几种类型(例如Storage)消耗,每种类型也需要异步初始化(静态,不是每个实例,并且这些初始化也依赖于Connections).最后,我的逻辑类型(例如Logic)使用这些存储实例.目前使用Simple Injector.
我尝试了几种不同的解决方案,但总有一种反模式存在.
我目前使用的解决方案有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);
}
}
Run Code Online (Sandbox Code Playgroud)
我已经将Temporal Coupling封装成一种方法,所以它并没有那么糟糕.但是,它仍然是一个反模式,而不是像我想的那样可维护.
常见的解决方案是抽象工厂模式.但是,在这种情况下,我们正在处理异步初始化.因此,我可以通过强制初始化同步运行来使用抽象工厂,但这会采用同步异步反模式.我真的不喜欢异步同步方法,因为我有几个存储空间,在我当前的代码中,它们都是同时初始化的; 由于这是一个云应用程序,将其更改为串行同步会增加启动时间,并且由于资源消耗,并行同步也不理想.
我也可以使用Abstract Factory和异步工厂方法.但是,这种方法存在一个主要问题.正如马克·西曼(Mark Seeman)在此评论的那样,"如果你正确注册它,任何值得盐的DI容器都能为你自动连接[工厂]实例." 不幸的是,对于异步工厂来说这是完全不正确的:AFAIK 没有支持这种情况的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);
}
Run Code Online (Sandbox Code Playgroud)
这导致了它自己的几个问题:
CreateAsync.因此,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();
}
}
Run Code Online (Sandbox Code Playgroud)
这里的问题是我们回到了时间耦合,这次遍布整个系统.此外,此方法要求所有公共成员都是异步方法.
那么,真的有两个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();
}
Run Code Online (Sandbox Code Playgroud)
Ste*_*ven 17
您遇到的问题以及您正在构建的应用程序是典型的问题.这是典型的两个原因:
但是,即使在您的情况下,解决方案也相当简单和优雅:
从包含它的类中提取初始化,并将初始化移动到组合根中.此时,您可以在将这些类注册到容器中之前创建并初始化这些类,并将这些初始化的类作为注册的一部分提供给容器.
这在您的特定情况下运行良好,因为您想要进行一些(一次性)启动初始化.启动初始化通常在配置容器之前完成,有时在需要完全组合的对象图之后完成.在我见过的大多数情况下,可以在之前完成初始化,这可以在您的情况下有效地完成.
正如我所说,与常规相比,你的情况有点奇怪.规范是:
通常,异步启动初始化没有任何实际好处.没有实际的性能优势,因为在启动时,无论如何都只会运行一个线程(尽管我们可能并行化,但显然不需要异步).另请注意,尽管某些应用程序类型在执行异步同步时可能会死锁,但在组合根中,我们确切地知道我们正在使用哪种应用程序类型以及这是否是一个问题.组合根是特定于应用程序的.换句话说,当我们在Composition Root中进行初始化时,异步执行启动初始化通常没有任何好处.
因为在Composition Root中我们知道同步异步是否是一个问题,我们甚至可以决定在第一次使用和同步时进行初始化.因为初始化量是有限的(与每个请求初始化相比),如果我们愿意,在具有同步阻塞的后台线程上执行它不会产生实际的性能影响.我们所要做的就是在Composition Root中定义一个Proxy类,确保初次使用时完成初始化.这就是Mark Seemann提出的回答.
我对Azure Functions一点都不熟悉,所以这实际上是第一个我知道实际上支持异步初始化的应用程序类型(当然除了Console应用程序).在大多数框架类型中,用户根本无法异步执行此启动初始化.例如,当我们Application_Start在ASP.NET应用程序或ASP.NET核心应用程序的Startup类中的事件中时,没有异步.一切都必须是同步的.
最重要的是,应用程序框架不允许我们异步构建其框架根组件.即使DI Containers支持进行异步解析的概念,由于缺乏对应用程序框架的支持,这也行不通.以ASP.NET Core Startup为例.它的IControllerActivator方法允许我们组成一个Controller实例,但返回类型Create(ControllerContext)不是Controller.换句话说,即使DI Containers为我们提供了一个Create方法,它仍然会导致阻塞,因为object调用将被包含在同步框架抽象之后.
在大多数情况下,您将看到初始化是按实例或在运行时完成的.例如,SqlConnections通常按请求打开,因此每个请求都需要打开自己的连接.当我们想要"及时"打开连接时,这不可避免地导致异步的应用程序接口.但请注意:
如果我们创建一个同步的实现,我们应该只使它的抽象同步,以防我们确定永远不会有另一个异步的实现(或代理,装饰器,拦截器等).如果我们无效地使抽象同步(即具有不暴露的方法和属性Task<object>),我们很可能会有一个漏洞抽象.当我们稍后进行异步实现时,这可能会导致我们在整个应用程序中进行彻底的更改.
换句话说,随着异步的引入,我们必须更加关注应用程序抽象的设计.这适用于您的情况.即使您现在可能只需要启动初始化,您确定对于您定义的抽象(以及ResolveAsync同样),您永远不需要及时的异步初始化吗?如果同步行为ResolveAsync是实现细节,则必须立即使其异步.
另一个例子是你的INugetRepository.它的成员是同步的,但这显然是一个漏洞抽象,因为它是同步的原因是因为它的实现是同步的.然而,它的实现是同步的,因为它使用了仅具有同步API的传统NuGet NuGet包.很明显SqlConnection,即使它的实现是同步的,也应该是完全异步的.
在应用异步的应用程序中,大多数应用程序抽象将主要具有异步成员.在这种情况下,将这种即时初始化逻辑同步化也是明智之举; 一切都已经异步.
总结一下:
虽然我很确定以下不是你想要的,但你能解释为什么它没有解决你的问题吗?
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; }
}
}
Run Code Online (Sandbox Code Playgroud)
为了保持设计清晰,我只实现了一个云属性,但另外两个可以以类似的方式完成.
该AzureConnections构造不会阻止,即使这需要显著的时间来初始化各种云的对象.
另一方面,它将启动工作,并且由于.NET任务的行为类似于promises,因此当您第一次尝试访问该值(使用Result)时,它将返回由此生成的值InitializeStorageAccount.
我得到的印象是,这不是你想要的,但由于我不明白你要解决的问题,我想我会留下这个答案,所以至少我们有一些东西要讨论.