对于需要异步初始化的类型,避免使用所有DI反模式 [英] Avoiding all DI antipatterns for types requiring asynchronous initialization

查看:113
本文介绍了对于需要异步初始化的类型,避免使用所有DI反模式的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的类型为 Connections ,需要异步初始化。此类型的实例由其他几种类型使用(例如,存储),每种类型都还需要异步初始化(静态的,不是按实例的,这些初始化也取决于在连接上)。最后,我的逻辑类型(例如 Logic )占用了这些存储实例。目前正在使用Simple Injector。



我尝试了几种不同的解决方案,但是始终存在反模式。






显式初始化(​​时间耦合)



我当前使用的解决方案具有时间耦合反模式:

 公共密封类连接
{
Task InitializeAsync();
}

公共密封类存储:IStorage
{
公共存储(连接数);
公共静态任务InitializeAsync(Connections connections);
}

公共密封类Logic
{
public Logic(IStorage storage);
}

公共静态类GlobalConfig
{
public static async Task SecureInitialized()
{
var connections = Container.GetInstance< Connections> ;();
等待连接。InitializeAsync();
等待Storage.InitializeAsync(connections);
}
}

我已将时间耦合封装为一个方法,因此它并不像可能的那样糟糕。






Abstract Factory(Sync-Over-异步)



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






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



我也可以将抽象工厂与异步工厂方法一起使用。但是,这种方法存在一个主要问题。正如Mark Seeman在此处所评论的那样,有价值的任何DI容器都将能够自动连接[factory]实例以您是否正确注册。不幸的是,这对于异步工厂是完全不正确的:AFAIK没有没有 DI容器对此提供支持。



因此,抽象异步工厂解决方案将要求我使用显式工厂,至少要使用 Func< Task< T>> 这最终到处都是(我们个人认为,默认情况下允许注册Func代表是一种设计气味...如果您您的系统中有很多依赖Func的构造函数,请仔细看一下您的依赖策略。):

  public密封类Connections 
{
private Connections();
public static Task< Connections> CreateAsync();
}

公共密封类存储:IStorage
{
//内部使用静态惰性进行我自己的静态初始化
public static Task< Storage> CreateAsync(Func< Task< Connections>>连接);
}

公共密封类Logic
{
public Logic(Func< Task< IStorage>>存储);
}

这会带来一些自身的问题:


  1. 我所有的工厂注册都必须显式地将依赖项从容器中拉出,并将其传递给 CreateAsync 。因此,DI容器不再做依赖注入

  2. 这些工厂调用的结果的生命周期不再由DI容器管理。现在,每个工厂负责生命周期管理,而不是DI容器。 (对于同步的Abstract Factory,如果工厂注册正确,这不是问题。)

  3. 任何实际使用这些依赖项的方法都需要异步-因为甚至逻辑方法也必须等待存储/连接初始化完成。在这个应用程序上,这对我来说不是什么大不了的事情,因为无论如何我的存储方法都是异步的,但是在一般情况下这可能是个问题。






自我初始化(时间耦合)



另一种(较不常见)的解决方案是让a的每个成员类型等待其自己的初始化:

 公共密封类连接
{
private Task InitializeAsync(); //内部使用Lazy

//曾经是属性BobConnection
public X GetBobConnectionAsync()
{
等待InitializeAsync();
返回BobConnection;
}
}

公共密封类存储:IStorage
{
public Storage(Connections connections);
private static Task InitializeAsync(Connections connections); //在内部使用惰性
公共异步任务< Y> IStorage.GetAsync()
{
等待InitializeAsync(_connections);
var connection =等待_connections.GetBobConnectionAsync();
返回等待连接。GetYAsync();
}
}

公共密封类Logic
{
public Logic(IStorage storage);
公共异步任务< Y> GetAsync()
{
return wait _storage.GetAsync();
}
}

这里的问题是我们回到了临时的耦合,这个时间分布在整个系统中。而且,这种方法要求 all 所有公共成员都是异步方法。






因此,




  • 消费者真的希望能够注入准备使用的实例。
  • >
  • DI容器大力推动简单的构造器



问题是-特别是对于异步初始化-如果DI容器对简单构造函数方法持强硬态度,则它们只是迫使用户在其他地方进行自己的初始化,这带来了自己的反模式。例如,为什么简单注入器不会考虑异步功能:否,此类功能不会对于Simple Injector或任何其他DI容器来说,是有意义的,因为在依赖注入时,它违反了一些重要的基本规则。但是,严格按照基本规则演奏显然会迫使其他反模式看起来更糟。



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




更新: AzureConnections 的完整签名(在上文中称为连接):

 公共密封类AzureConnections 
{
public AzureConnections();

public CloudStorageAccount CloudStorageAccount {get; }
public CloudBlobClient CloudBlobClient {get; }
public CloudTableClient CloudTableClient {get; }

公共异步任务InitializeAsync();
}


解决方案

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


  1. 您需要(或想要)异步启动初始化,以及

  2. 您的应用程序框架(azure函数)支持异步启动初始化(或者,似乎没有围绕它的框架)。
    这使您的情况与典型情况有所不同,这可能使讨论共同模式变得更加困难。

但是,即使在您的情况下,解决方案也非常简单而优雅:



从包含它的类中提取初始化,并将其移到组成根。此时,您可以在 将它们注册到容器中之前创建并初始化这些类,并将这些初始化后的类作为注册的一部分送入容器。



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



正如我所说,相比之下,您的情况有点奇怪规范。规范是:




  • 启动初始化是同步的。框架(如ASP.NET Core)通常在启动阶段不支持异步初始化

  • 初始化通常需要按请求和即时进行,而不是按时间进行。应用程序和提前。通常,需要初始化的组件的寿命很短,这意味着我们通常会在首次使用时初始化此类实例(换句话说:及时)。



异步初始化启动通常没有真正的好处。这对实际性能没有任何好处,因为在启动时,无论如何只有一个线程正在运行(尽管我们可以并行化它,但显然不需要异步)。还要注意,尽管某些应用程序类型可能在执行异步同步时陷入僵局,但是在合成根中,我们确切地知道了我们正在使用哪种应用程序类型,以及这是否会成为问题。合成根总是 特定于应用程序。换句话说,当我们在无死锁的应用程序(例如ASP.NET Core,Azure Functions等)的组成根中进行初始化时,通常没有异步执行启动初始化的好处。



由于在合成根目录中,我们知道sync-over-async是否存在问题,我们甚至可以决定在首次使用时进行同步并进行同步。因为初始化的数量是有限的(与每个请求的初始化相比),所以如果希望的话,在具有同步阻塞的后台线程上进行初始化不会对实际性能产生影响。我们要做的就是在我们的合成根中定义一个Proxy类,以确保首次使用时完成初始化。



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



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



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



如果我们创建一个同步的实现,则只有在确定有永远不要成为异步的另一个实现(或代理,装饰器,拦截器等)。如果我们无效地使抽象同步(即,具有不公开 Task< T> 的方法和属性),则很可能会有泄漏抽象在手。稍后当我们获得异步实现时,这可能迫使我们在整个应用程序中进行全面更改。



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



另一个示例是您的。它的成员是同步的,但这显然是Leaky Abstraction,因为它是同步的,是因为它的实现是同步的。但是,其实现是同步的,因为它利用了仅具有同步API的旧版NuGet NuGet程序包。显然,即使 INugetRepository 应该完全异步,即使其实现是同步的。



在适用的应用程序中异步,大多数应用程序抽象将主要具有异步成员。在这种情况下,使这种及时的初始化逻辑也异步就可以了。



总结一下:




I have a type Connections that requires asynchronous initialization. An instance of this type is consumed by several other types (e.g., Storage), each of which also require asynchronous initialization (static, not per-instance, and these initializations also depend on Connections). Finally, my logic types (e.g., Logic) consumes these storage instances. Currently using Simple Injector.

I've tried several different solutions, but there's always an antipattern present.


Explicit Initialization (Temporal Coupling)

The solution I'm currently using has the Temporal Coupling antipattern:

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

I've encapsulated the Temporal Coupling into a method, so it's not as bad as it could be. But still, it's an antipattern and not as maintainable as I'd like.


Abstract Factory (Sync-Over-Async)

A common proposed solution is an Abstract Factory pattern. However, in this case we're dealing with asynchronous initialization. So, I could use Abstract Factory by forcing the initialization to run synchronously, but this then adopts the sync-over-async antipattern. I really dislike the sync-over-async approach because I have several storages and in my current code they're all initialized concurrently; since this is a cloud application, changing this to be serially synchronous would increase startup time, and parallel synchronous is also not ideal due to resource consumption.


Asynchronous Abstract Factory (Improper Abstract Factory Usage)

I can also use Abstract Factory with asynchronous factory methods. However, there's one major problem with this approach. As Mark Seeman comments here, "Any DI Container worth its salt will be able to auto-wire an [factory] instance for you if you register it correctly." Unfortunately, this is completely untrue for asynchronous factories: AFAIK there is no DI container that supports this.

So, the Abstract Asynchronous Factory solution would require me to use explicit factories, at the very least Func<Task<T>>, and this ends up being everywhere ("We personally think that allowing to register Func delegates by default is a design smell... If you have many constructors in your system that depend on a Func, please take a good look at your dependency strategy."):

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

This causes several problems of its own:

  1. All my factory registrations have to pull dependencies out of the container explicitly and pass them to CreateAsync. So the DI container is no longer doing, you know, dependency injection.
  2. The results of these factory calls have lifetimes that are no longer managed by the DI container. Each factory is now responsible for lifetime management instead of the DI container. (With the synchronous Abstract Factory, this is not an issue if the factory is registered appropriately).
  3. Any method actually using these dependencies would need to be asynchronous - since even the logic methods must await for the storage/connections initialization to complete. This is not a big deal for me on this app since my storage methods are all asynchronous anyway, but it can be a problem in the general case.


Self Initialization (Temporal Coupling)

Another, less common, solution is to have each member of a type await its own initialization:

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

The problem here is that we're back to Temporal Coupling, this time spread out throughout the system. Also, this approach requires all public members to be asynchronous methods.


So, there's really two DI design perspectives that are at odds here:

  • Consumers want to be able to inject instances that are ready to use.
  • DI containers push hard for simple constructors.

The problem is - particularly with asynchronous initialization - that if DI containers take a hard line on the "simple constructors" approach, then they are just forcing the users to do their own initialization elsewhere, which brings its own antipatterns. E.g., why Simple Injector won't consider asynchronous functions: "No, such feature does not make sense for Simple Injector or any other DI container, because it violates a few important ground rules when it comes to dependency injection." However, playing strictly "by the ground rules" apparently forces other antipatterns that seem much worse.

The question: is there a solution for asynchronous initialization that avoids all antipatterns?


Update: Complete signature for AzureConnections (referred to above as Connections):

public sealed class AzureConnections
{
  public AzureConnections();

  public CloudStorageAccount CloudStorageAccount { get; }
  public CloudBlobClient CloudBlobClient { get; }
  public CloudTableClient CloudTableClient { get; }

  public async Task InitializeAsync();
}

解决方案

The problem you have, and the application you're building, is a-typical. It’s a-typical for two reasons:

  1. you need (or rather want) asynchronous start-up initialization, and
  2. Your application framework (azure functions) supports asynchronous start-up initialization (or rather, there seems to be little framework surrounding it). This makes your situation a bit different from a typical scenario, which might make it a bit harder to discuss common patterns.

However, even in your case the solution is rather simple and elegant:

Extract initialization out of the classes that hold it, and move it into the Composition Root. At that point you can create and initialize those classes before registering them in the container and feed those initialized classes into the container as part of registrations.

This works well in your particular case, because you want to do some (one-time) start-up initialization. Start-up initialization is typically done before you configure the container (or sometimes after if it requires a fully composed object graph). In most cases I’ve seen, initialization can be done before, as can be done effectively in your case.

As I said, your case is a bit peculiar, compared to the norm. The norm is:

  • Start-up initialization is synchronous. Frameworks (like ASP.NET Core) typically do not support asynchronous initialization in the start-up phase
  • Initialization often needs to be done per-request and just-in-time rather than per-application and ahead-of-time. Often components that need initialization have a short lifetime, which means we typically initialize such instance on first use (in other words: just-in-time).

There is usually no real benefit of doing start-up initialization asynchronously. There is no practical performance benefit because, at start-up time, there will only be a single thread running anyway (although we might parallelize this, that obviously doesn’t require async). Also note that although some application types might deadlock on doing synch-over-async, in the Composition Root we know exactly which application type we are using and whether or not this will be a problem or not. A Composition Root is always application-specific. In other words, when we have initialization in the Composition Root of a non-deadlocking application (e.g. ASP.NET Core, Azure Functions, etc), there is typically no benefit of doing start-up initialization asynchronously.

Because in the Composition Root we know whether or not sync-over-async is a problem or not, we could even decide to do the initialization on first use and synchronously. Because the amount of initialization is finite (compared to per-request initialization) there is no practical performance impact on doing it on a background thread with synchronous blocking if we wish. All we have to do is define a Proxy class in our Composition Root that makes sure that initialization is done on first use. This is pretty much the idea that Mark Seemann proposed as answer.

I was not familiar at all with Azure Functions, so this is actually the first application type (except Console apps of course) that I know of that actually supports async initialization. In most framework types, there is no way for users to do this start-up initialization asynchronously at all. When we’re inside an Application_Start event in an ASP.NET application or in the Startup class of an ASP.NET Core application, for instance, there is no async. Everything has to be synchronous.

On top of that, application frameworks don’t allow us to build their framework root components asynchronously. So even if DI Containers would support the concept of doing asynchronous resolves, this wouldn’t work because of the ‘lack’ of support of application frameworks. Take ASP.NET Core’s IControllerActivator for instance. Its Create(ControllerContext) method allows us to compose a Controller instance, but the return type of the Create method is object, not Task<object>. In other words, even if DI Containers would provide us with a ResolveAsync method, it would still cause blocking because ResolveAsync calls would be wrapped behind synchronous framework abstractions.

In the majority of cases, you’ll see that initialization is done per-instance or at runtime. A SqlConnection, for instance, is typically opened per request, so each request needs to open its own connection. When we want to open the connection ‘just in time’, this inevitably results in application interfaces that are asynchronous. But be careful here:

If we create an implementation that is synchronous, we should only make its abstraction synchronous in case we are sure that there will never be another implementation (or proxy, decorator, interceptor, etc.) that is asynchronous. If we invalidly make the abstraction synchronous (i.e. have methods and properties that do not expose Task<T>), we might very well have a Leaky Abstraction at hand. This might force us to make sweeping changes throughout the application when we get an asynchronous implementation later on.

In other words, with the introduction of async we have to take even more care of the design of our application abstractions. This holds for your case as well. Even though you might only require start-up initialization now, are you sure that for the abstractions you defined (and AzureConnections as well) will never need just-in-time async initialization? In case the synchronous behavior of AzureConnections is an implementation detail, you will have to make it async right away.

Another example of this is your INugetRepository. Its members are synchronous, but that is clearly a Leaky Abstraction, because the reason it is synchronous is because its implementation is synchronous. Its implementation, however, is synchronous because it makes use of a legacy NuGet NuGet package that only has a synchronous API. It’s pretty clear that INugetRepository should be completely async, even though its implementation is synchronous.

In an application that applies async, most application abstractions will have mostly async members. When this is the case, it would be a no-brainer to make this kind of just-in-time initialization logic async as well; everything is already async.

To summarize:

  • In case you need start-up initialization: do it before or after configuring the container. This makes composing object graphs itself fast, reliable, and verifiable.
  • Doing initialization before configuring the container prevents Temporal Coupling, but might mean you will have to move initialization out of the classes that require it (which is actually a good thing).
  • Async start-up initialization is impossible in most application types. In the other application types it is typically unnecessary.
  • In case you require per-request or just-in-time initialization, there is no way around having asynchronous interfaces.
  • Be careful with synchronous interfaces if you’re building an asynchronous application, you might be leaking implementation details.

这篇关于对于需要异步初始化的类型,避免使用所有DI反模式的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆