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

时间:2017-08-28 17:01:16

标签: dependency-injection initialization async-await simple-injector abstract-factory

我的类型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);
  }
}

我已经将Temporal Coupling封装成一种方法,所以它并没有那么糟糕。但是,它仍然是一个反模式,而不是像我一样可维护。

抽象工厂(Sync-Over-Async)

常见的解决方案是抽象工厂模式。但是,在这种情况下,我们正在处理异步初始化。因此,我可以通过强制初始化同步运行来使用抽象工厂,但是这会采用同步异步反模式。我真的不喜欢异步同步方法,因为我有几个存储空间,在我当前的代码中,它们全部同时初始化;由于这是一个云应用程序,将其更改为串行同步会增加启动时间,并且由于资源消耗,并行同步也不理想。

异步抽象工厂(不正确的抽象工厂用法)

我也可以使用Abstract Factory和异步工厂方法。然而,这种方法存在一个主要问题。正如Mark Seeman评论here,&#34;如果您正确注册,任何值得盐的DI容器都可以为您自动连接[工厂]实例。&#34;不幸的是,对于异步工厂来说这是完全不正确的:AFAIK有 no DI容器支持它。

因此,抽象异步工厂解决方案需要我使用显式工厂,至少Func<Task<T>>this ends up being everywhere(&#34;我们个人认为允许默认注册Func委托是一种设计气味...如果您的系统中有许多依赖于Func的构造函数,请仔细查看您的依赖策略。&#34;):

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);
}

这导致了几个问题:

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

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

    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容器在&#34;简单的构造函数&#34;方法,然后他们只是强迫用户在其他地方进行自己的初始化,这带来了自己的反模式。例如,why Simple Injector won't consider asynchronous functions:&#34;不,这样的功能对于简单注入器或任何其他DI容器没有意义,因为它涉及依赖注入时违反了一些重要的基本规则。&#34;但是,严格遵守基本规则&#34;显然迫使其他反模式看起来更糟糕。

    问题:是否存在避免所有反模式的异步初始化解决方案?

    更新:AzureConnections的完整签名(以上称为Connections):

    public sealed class AzureConnections
    {
      public AzureConnections();
    
      public CloudStorageAccount CloudStorageAccount { get; }
      public CloudBlobClient CloudBlobClient { get; }
      public CloudTableClient CloudTableClient { get; }
    
      public async Task InitializeAsync();
    }
    

3 个答案:

答案 0 :(得分:15)

您遇到的问题the application you're building是典型问题。这是典型的两个原因:

  1. 您需要(或者更喜欢)异步启动初始化和
  2. 您的应用程序框架(azure函数)支持异步启动初始化(或者更确切地说,围绕它的框架似乎很少)。 这使得您的情况与典型情况略有不同,这可能会使讨论常见模式变得更加困难。
  3. 然而,即使在您的情况下,解决方案也相当简单和优雅:

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

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

    正如我所说,与常规相比,你的情况有点特殊。标准是:

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

    异步启动初始化通常没有什么好处。没有实际的性能优势,因为在启动时,无论如何都只会运行一个线程(尽管我们可能并行化,显然不需要异步)。另请注意,尽管某些应用程序类型在执行异步同步时可能会死锁,但在组合根中我们知道完全我们正在使用哪种应用程序类型以及这是否是一个问题。组合根始终特定于应用程序。换句话说,当我们在非死锁应用程序的组合根中进行初始化(例如ASP.NET Core,Azure Functions等)时,异步启动初始化通常没有任何好处。

    因为在Composition Root中我们知道同步异步是否是一个问题,我们甚至可以决定在第一次使用和同步时进行初始化。因为初始化量是有限的(与每个请求初始化相比),如果我们愿意,在具有同步阻塞的后台线程上执行它不会产生实际的性能影响。我们所要做的就是在Composition Root中定义一个Proxy类,确保初次使用时完成初始化。这就是Mark Seemann提出的回答。

    我对Azure Functions一点都不熟悉,所以这实际上是我知道的第一个实际支持异步初始化的应用程序类型(当然除了Console应用程序)。在大多数框架类型中,用户根本无法异步执行此启动初始化。例如,当我们在ASP.NET应用程序中的Application_Start事件或ASP.NET核心应用程序的Startup类中时,没有异步。一切都必须是同步的。

    最重要的是,应用程序框架不允许我们异步构建其框架根组件。因此,即使DI Containers支持执行异步解析的概念,由于缺乏对应用程序框架的支持,这也行不通。以ASP.NET Core的IControllerActivator为例。其Create(ControllerContext)方法允许我们撰写Controller实例,但Create方法的返回类型为object,而不是Task<object>。换句话说,即使DI容器为我们提供ResolveAsync方法,它仍然会导致阻塞,因为ResolveAsync调用将包含在同步框架抽象之后。

    在大多数情况下,您将看到初始化是按实例或在运行时完成的。例如,SqlConnection通常按请求打开,因此每个请求都需要打开自己的连接。当我们想要“及时”打开连接时,这不可避免地导致异步的应用程序接口。但请注意:

    如果我们创建一个同步的实现,我们应该只在我们确定 never 是另一个实现(或代理,装饰器,拦截器等)的情况下使其抽象同步。)这是异步的。如果我们无效地使抽象同步(即具有不公开Task<T>的方法和属性),我们可能会有一个 Leaky Abstraction 。当我们稍后进行异步实现时,这可能会迫使我们在整个应用程序中进行彻底的更改。

    换句话说,随着异步的引入,我们必须更加关注应用程序抽象的设计。这适用于您的情况。即使您现在可能只需要启动初始化,您确定对于您定义的抽象(以及AzureConnections)将永远不需要即时异步初始化吗?如果AzureConnections的同步行为是实现细节,则必须立即使其异步。

    另一个例子是你的INugetRepository。它的成员是同步的,但这显然是一个漏洞抽象,因为它是同步的原因是因为它的实现是同步的。然而,它的实现是同步的,因为它使用了只有一个同步API的传统NuGet NuGet包。很明显INugetRepository应该是完全异步的,即使它的实现是同步的。

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

    总结:

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

答案 1 :(得分: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; }
    }
}

为了保持设计清晰,我只实现了一个云属性,但另外两个可以以类似的方式完成。

AzureConnections构造函数不会阻塞,即使它需要很长时间来初始化各种云对象。

另一方面,它将启动工作,并且由于.NET任务的行为类似于promises,因此当您第一次尝试访问该值(使用Result)时,它将返回{生成的值} {1}}。

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

答案 2 :(得分:0)

您似乎正在尝试使用代理单例类做我正在做的事情。

                services.AddSingleton<IWebProxy>((sp) => 
                {
                    //Notice the GetService outside the Task.  It was locking when it was inside
                    var data = sp.GetService<IData>();

                    return Task.Run(async () =>
                    {
                        try
                        {
                            var credentials = await data.GetProxyCredentialsAsync();
                            if (credentials != null)
                            {
                                return new WebHookProxy(credentials);
                            }
                            else
                            {
                                return (IWebProxy)null;
                            }
                        }
                        catch(Exception ex)
                        {
                            throw;
                        }
                    }).Result;  //Back to sync
                });