gpt4 book ai didi

dependency-injection - 对于需要异步初始化的类型,避免使用所有DI反模式

转载 作者:行者123 更新时间:2023-12-03 09:30:14 25 4
gpt4 key购买 nike

我有一个Connections类型,需要异步初始化。此类型的实例由其他几种类型(例如Storage)使用,每种类型都还需要异步初始化(静态,而不是按实例,并且这些初始化也取决于Connections)。最后,我的逻辑类型(例如Logic)使用了这些存储实例。当前正在使用简单注入(inject)器。

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

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

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

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

我已经将“时间耦合”封装到一个方法中,因此它没有它可能的糟糕。但是,它仍然是一种反模式,并且不像我想要的那样易于维护。

抽象工厂(异步同步)

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

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

我也可以将Abstract Factory与异步工厂方法一起使用。但是,这种方法存在一个主要问题。正如Mark Seeman评论 here一样,“只要正确注册,任何值得其盐分的DI容器都可以为您自动连接[工厂]实例。”不幸的是,这对于异步工厂是完全不正确的:AFAIK没有支持此功能的DI容器。

因此,抽象异步工厂解决方案将要求我使用显式工厂,至少要使用 Func<Task<T>>this ends up being everywhere(“我们个人认为默认情况下允许注册Func委托(delegate)是一种设计气味……如果您的构造函数很多,依赖于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容器不再做依赖注入(inject)。
  • 这些工厂调用的结果的生命周期不再由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();
    }
    }

    这里的问题是我们回到了时间耦合,这一次遍及整个系统。同样,此方法要求所有公共(public)成员都是异步方法。

    因此,这里确实有两种DI设计观点是矛盾的:
  • 消费者希望能够注入(inject)准备使用的实例。
  • DI容器竭力争取simple constructors

  • 问题是-尤其是对于异步初始化-如果DI容器对“简单构造函数”方法持强硬态度,那么它们只是迫使用户在其他地方进行自己的初始化,这带来了自己的反模式。例如, why Simple Injector won't consider asynchronous functions:“否,这种功能对于Simple Injector或任何其他DI容器没有意义,因为它在依赖项注入(inject)方面违反了一些重要的基本规则。”但是,严格按照“基本规则”演奏显然会迫使其他反模式看起来更糟。

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

    更新: AzureConnections的完整签名(上面称为 Connections):
    public sealed class AzureConnections
    {
    public AzureConnections();

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

    public async Task InitializeAsync();
    }

    最佳答案

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

    您需要(或希望)异步启动初始化的

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

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

    从保存初始化的类中提取初始化,然后将其移动到Composition Root中。那时,您可以在将这些类注册到容器中之前创建并初始化这些类,并将这些已初始化的类作为注册的一部分馈入容器。

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

    正如我所说,与正常情况相比,您的案件有些特殊。规范是:
  • 启动初始化是同步的。框架(如ASP.NET Core)通常在启动阶段
  • 不支持异步初始化
  • 通常需要按请求和及时完成初始化,而不是按应用程序和提前完成初始化。通常,需要初始化的组件的寿命很短,这意味着我们通常会在首次使用时初始化此类实例(换句话说:及时)。

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

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

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

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

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

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

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

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

    在应用异步的应用程序中,大多数应用程序抽象将主要具有异步成员。在这种情况下,使这种及时的初始化逻辑也异步将是理所当然的。一切都已经异步了。

    总结一下:
  • 如果需要启动初始化:在配置容器之前或之后进行。这使得组成对象图本身快速,可靠和可验证。
  • 在配置容器之前进行初始化会阻止Temporal Coupling,但可能意味着您必须将初始化从需要它的类中移出(这实际上是一件好事)。
  • 在大多数应用程序类型中,异步启动初始化是不可能的。在其他应用程序类型中,通常没有必要。
  • 如果您需要按请求或即时初始化,则无法使用异步接口(interface)。
  • 如果要构建异步应用程序,请谨慎使用同步接口(interface),否则可能会泄漏实现细节。
  • 关于dependency-injection - 对于需要异步初始化的类型,避免使用所有DI反模式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45924027/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com