我尝试了几种不同的解决方案,但总有一种反模式存在。
显式初始化(时间耦合)
我目前使用的解决方案有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); } }
我将Temporal Coupling封装成一种方法,所以它并没有那么糟糕。但是,它仍然是一个反模式,而不是像我想的那样可维护。
抽象工厂(同步异步)
常见的解决方案是抽象工厂模式。但是,在这种情况下,我们正在处理异步初始化。因此,我可以通过强制初始化同步运行来使用抽象工厂,但这会采用sync-over-async反模式。我真的不喜欢异步同步方法,因为我有几个存储空间,在我当前的代码中,它们都是同时初始化的;由于这是一个云应用程序,将其更改为串行同步会增加启动时间,并且由于资源消耗,并行同步也不理想。
异步抽象工厂(不正确的抽象工厂用法)
我也可以使用Abstract Factory和异步工厂方法。但是,这种方法存在一个主要问题。正如马克·西曼(Mark Seeman)评论here所说的那样,“如果你正确注册,任何值得盐的DI容器都能为你自动连接[工厂]实例。”不幸的是,对于异步工厂来说这是完全不正确的:AFAIK没有支持这种情况的DI容器。
因此,抽象异步工厂解决方案将要求我使用显式工厂,至少Func< Task< T>>和this ends up being everywhere(“我们个人认为允许默认注册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容器为simple constructors努力推进。
问题是 – 特别是对于异步初始化 – 如果DI容器对“简单构造函数”方法采取强硬路线,那么它们只是强迫用户在其他地方进行自己的初始化,这带来了自己的反模式。例如,why Simple Injector won’t consider asynchronous functions:“不,这种功能对于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(); }
>您需要(或者更确切地想要)异步启动初始化,以及
>您的应用程序框架(azure函数)支持异步启动初始化(或者更确切地说,围绕它的框架似乎很少)。
这使得您的情况与正常情况略有不同,这可能会使讨论常见模式变得更加困难。
但是,即使在您的情况下,解决方案也相当简单和优雅:
从包含它的类中提取初始化,并将初始化移动到组合根中。此时,您可以在将这些类注册到容器中之前创建并初始化这些类,并将这些初始化的类作为注册的一部分提供给容器。
这在您的特定情况下运行良好,因为您想要进行一些(一次性)启动初始化。启动初始化通常在配置容器之前完成,或者有时在需要完全组合的对象图之后完成。在我见过的大多数情况下,可以在之前完成初始化,这可以在您的情况下有效地完成。
正如我所说,与常规相比,你的情况有点奇怪。标准是:
>启动初始化是同步的。框架(如ASP.NET Core)通常不支持启动阶段的异步初始化
>初始化通常需要按时,按时,而不是按应用程序提前完成。通常需要初始化的组件具有较短的生命周期,这意味着我们通常在首次使用时初始化此类实例(换句话说:实时)。
通常,异步启动初始化没有任何实际好处。没有实际的性能优势,因为在启动时,无论如何都只会运行一个线程(尽管我们可能会将其并行化,但显然不需要异步)。另请注意,虽然某些应用程序类型可能会在执行异步同步时死锁,但在组合根中,我们确切地知道我们正在使用哪种应用程序类型以及这是否是一个问题。组合根是特定于应用程序的。换句话说,当我们在Composition Root中进行初始化时,通常不会异步进行启动初始化。
因为在Composition Root中我们知道同步异步是否是一个问题,我们甚至可以决定在第一次使用和同步时进行初始化。由于初始化量是有限的(与每个请求初始化相比),如果我们愿意,在具有同步阻塞的后台线程上执行它不会产生实际的性能影响。我们所要做的就是在Composition Root中定义一个Proxy类,确保初次使用时完成初始化。这就是Mark Seemann提出的回答。
我对Azure Functions一点都不熟悉,所以这实际上是我知道的第一个实际支持异步初始化的应用程序类型(当然除了Console应用程序)。在大多数框架类型中,用户根本无法异步执行此启动初始化。例如,当我们在ASP.NET应用程序或ASP.NET核心应用程序的Startup类中的Application_Start事件中时,没有异步。一切都必须是同步的。
最重要的是,应用程序框架不允许我们异步构建其框架根组件。即使DI Containers支持进行异步解析的概念,由于缺乏对应用程序框架的支持,这也行不通。以ASP.NET Core的IControllerActivator为例。它的Create(ControllerContext)方法允许我们组成一个Controller实例,但返回类型是object,而不是Task< object>。换句话说,即使DI Containers为我们提供了ResolveAsync方法,它仍然会导致阻塞,因为ResolveAsync调用将包含在同步框架抽象之后。
在大多数情况下,您将看到初始化是按实例或在运行时完成的。例如,sqlConnections通常按请求打开,因此每个请求都需要打开自己的连接。当我们想要“及时”打开连接时,这不可避免地导致异步的应用程序接口。但请注意:
如果我们创建一个同步的实现,我们应该只在我们确定永远不会有另一个异步的实现(或代理,装饰器,拦截器等)的情况下使它的抽象同步。如果我们无效地使抽象同步(即具有不暴露任务< T>的方法和属性),我们很可能会有一个漏洞抽象。当我们稍后进行异步实现时,这可能会导致我们在整个应用程序中进行彻底的更改。
换句话说,随着异步的引入,我们必须更加关注应用程序抽象的设计。这适用于您的情况。即使您现在可能只需要启动初始化,您确定对于您定义的抽象(以及AzureConnections),您永远不需要及时的异步初始化吗?如果AzureConnections的同步行为是实现细节,则必须立即使其异步。
另一个例子是你的INugetRepository.它的成员是同步的,但这显然是一个漏洞抽象,因为它是同步的原因是因为它的实现是同步的。然而,它的实现是同步的,因为它使用了只有一个同步API的传统NuGet NuGet包。很明显INugetRepository应该是完全异步的,即使它的实现是同步的。
在应用异步的应用程序中,大多数应用程序抽象将主要具有异步成员。在这种情况下,将这种即时初始化逻辑同步化也是一件很容易的事。一切都已经异步。
总结一下:
>如果您需要启动初始化:在配置容器之前或之后或之后执行。这使得组合对象图快速,可靠和可验证。>在配置容器之前进行初始化会阻止Temporal Coupling,但可能意味着您必须将初始化移出需要它的类(这实际上是一件好事)。>在大多数应用程序类型中,异步启动初始化是不可能的,在其他类型中通常不需要。>如果您需要按请求或即时初始化,则无法使用异步接口。>如果要构建异步应用程序,请注意同步接口,否则可能会泄漏实现细节。