避免所有DI反模式需要异步初始化的类型

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)

这导致了它自己的几个问题:

  1. 我的所有工厂注册必须明确地从容器中提取依赖关系并将它们传递给CreateAsync.因此,DI容器不再执行依赖注入.
  2. 这些工厂调用的结果具有不再由DI容器管理的寿命.现在每个工厂都负责终身管理,而不是DI容器.(使用同步抽象工厂,如果工厂已正确注册,这不是问题).
  3. 实际使用这些依赖项的任何方法都需要是异步的 - 因为即使是逻辑方法也必须等待存储/连接初始化完成.这对我来说对这个应用程序来说不是什么大问题,因为我的存储方法无论如何都是异步的,但在一般情况下它可能是个问题.

自初始化(时间耦合)

另一种不太常见的解决方案是让一个类型的每个成员等待它自己的初始化:

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

您遇到的问题以及您正在构建的应用程序是典型的问题.这是典型的两个原因:

  1. 你需要(或者更想要)异步启动初始化,以及
  2. 您的应用程序框架(azure函数)支持异步启动初始化(或者更确切地说,围绕它的框架似乎很少).这使得您的情况与正常情况略有不同,这可能会使讨论常见模式变得更加困难.

但是,即使在您的情况下,解决方案也相当简单和优雅:

从包含它的类中提取初始化,并将初始化移动到组合根中.此时,您可以在将这些类注册到容器中之前创建并初始化这些类,并将这些初始化的类作为注册的一部分提供给容器.

这在您的特定情况下运行良好,因为您想要进行一些(一次性)启动初始化.启动初始化通常在配置容器之前完成,有时在需要完全组合的对象图之后完成.在我见过的大多数情况下,可以在之前完成初始化,这可以在您的情况下有效地完成.

正如我所说,与常规相比,你的情况有点奇怪.规范是:

  • 启动初始化是同步的.框架(如ASP.NET Core)通常不支持启动阶段的异步初始化
  • 初始化通常需要按时,按时,而不是按应用程序提前完成.通常需要初始化的组件具有较短的生命周期,这意味着我们通常在首次使用时初始化此类实例(换句话说:实时).

通常,异步启动初始化没有任何实际好处.没有实际的性能优势,因为在启动时,无论如何都只会运行一个线程(尽管我们可能并行化,但显然不需要异步).另请注意,尽管某些应用程序类型在执行异步同步时可能会死锁,但在组合根中,我们确切地知道我们正在使用哪种应用程序类型以及这是否是一个问题.组合根是特定于应用程序的.换句话说,当我们在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,即使它的实现是同步的,也应该是完全异步的.

在应用异步的应用程序中,大多数应用程序抽象将主要具有异步成员.在这种情况下,将这种即时初始化逻辑同步化也是明智之举; 一切都已经异步.

总结一下:

  • 如果您需要启动初始化:在配置容器之前或之后或之后执行.这使得组合对象图快速,可靠和可验证.
  • 在配置容器之前进行初始化会阻止Temporal Coupling,但可能意味着您必须将初始化移出需要它的类(我实际上认为这是一件好事).
  • 在大多数应用程序类型中,异步启动初始化是不可能的,在其他类型中通常是不必要的.
  • 如果您需要按请求或即时初始化,则无法使用异步接口.
  • 如果要构建异步应用程序,请注意同步接口,否则可能会泄漏实现细节.


Mar*_*ann 5

虽然我很确定以下不是你想要的,但你能解释为什么它没有解决你的问题吗?

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.

我得到的印象是,这不是你想要的,但由于我不明白你要解决的问题,我想我会留下这个答案,所以至少我们有一些东西要讨论.