【问题标题】:Write an abstract factory implementation by using the ASP.NET core DI container: how to manage object lifetime?使用 ASP.NET core DI 容器编写抽象工厂实现:如何管理对象生命周期?
【发布时间】:2019-12-11 14:18:00
【问题描述】:

我们正在将庞大的代码库从 .NET 框架迁移到 .NET 核心。不幸的是,我们正在迁移的一些代码存在设计异味,但在这个阶段我们不能随意破坏,我们需要仔细计划随着时间的推移进行更改。

主要问题之一是我们的代码依赖于大量抽象工厂,这些工厂已知为a code smell(至少在被滥用时)。一个相关的问题是 Castle Windsor 容器在现有代码中被广泛使用,我们希望避免在 ASP.NET 核心中使用它,我们更愿意坚持使用默认内置的 DI 容器。

我试图了解是否可以通过使用 ASP.NET 核心 DI 容器来重写我们目前基于城堡温莎容器的一些抽象工厂实现。根据我对依赖注入的理解,在应用程序的组合根中编写这类类不是问题;换句话说,具有依赖于 DI 容器的组合根类不是一种引入service locator 代码气味的方法。

目前我们抽象工厂的接口定义是一个有漏洞的,因为它公开了一个Release 方法,该方法只是为了遵守Castle Windsor 的register resolve release pattern。这是当前的接口定义:

public interface ICommandHandlerFactory
{
  ICommandHandler CreateHandler(Type commandType);
  void Release(ICommandHandler handler);
}

具体实现依赖于 Castle Windsor 容器,以便解析命令处理程序并在用户完成时释放它们:

public class WindsorCommandHandlerFactory
{
   private readonly IKernel _container;

   public WindsorCommandHandlerFactory(IKernel container)
   {
      _container = container;
   }

   public ICommandHandler CreateHandler(Type commandType)
   {
     // here we create the command handler type from the command type and then
     // we ask the container to resolve the command handler type
   }

   public void Release(ICommandHandler handler)
   {
     _container.ReleaseComponent(handler);
   }
}

我的问题与对象生命周期管理有关。关键是对于 ASP.NET 核心 DI 容器,生命周期管理不是基于 Release 方法,而是基于 scope 的概念。

最佳实践基本上是从应用程序根容器创建一个作用域,从作用域的容器解析依赖关系,使用依赖关系并最终处置作用域。当作用域被释放时,作用域内解析的作用域和瞬态服务将被停用并避免内存泄漏。

这是对应的代码:

public class Worker 
{
  private readonly IServiceProvider container;
  
  public Worker(IServiceProvider container)
  {
    _container = container;
  }

  public void DoStuff()
  {
    using(var scope = container.CreateScope())
    {
      var service = scope.ServiceProvider.GetRequiredService<IService>();
      service.Work();
    }
  }
}

为了坚持这个设计,我应该清楚地简化抽象工厂定义,通过删除泄漏(现在不再有用)Release 方法:

public interface ICommandHandlerFactory
{
  ICommandHandler CreateHandler(Type commandType);
}

鉴于这个接口定义,我该如何应对容器作用域的创建和处置?

不能简单地做以下事情,因为当作用域被释放时,已解决的依赖关系被解除,因此调用代码可能会引用已释放的对象强>:

// this WON'T work due to the scope disposal when the service is returned
public class CommandHandlerFactory 
    {
      private readonly IServiceProvider container;
      
      public CommandHandlerFactory(IServiceProvider container)
      {
        _container = container;
      }
      
      public ICommandHandler CreateHandler(Type commandType)
      {
         using(var scope = container.CreateScope())
        {
          Type commandHandlerType = ... // build the command handler type starting from the command type
        
          var service = scope.ServiceProvider.GetRequiredService(commandHandlerType);
          return service;
        }
      }
    }

你有什么想法吗?

更新

再次仔细阅读this article后,我意识到这个设计问题的解决方案非常明显。

讨论的重点是抽象工厂设计模式的内在问题,所以解决的方法可能是完全避免抽象工厂

下面的代码非常自动解释,并展示了一种用另一个抽象(在示例中称为ICommandDispatcher)替换抽象工厂的方法,该抽象基本上是一个适配器,用于为要处理的命令调用正确的命令处理程序。

public interface ICommand 
  {
  }

  public interface ICommandHandler<T> where T : ICommand
  {
    void Handle(T command);
  }

  public interface ICommandDispatcher
  {
    void Dispatch(ICommand command);
  }

  public class CommandDispatcher : ICommandDispatcher
  {
    private readonly IServiceProvider _container;

    public CommandDispatcher(IServiceProvider container)
    {
      _container = container ?? throw new ArgumentNullException(nameof(container));
    }

    public void Dispatch(ICommand command)
    {
      if (command == null)
        throw new ArgumentNullException(nameof(command));

      var commandType = command.GetType();
      var commandHandlerType = typeof(ICommandHandler<>).MakeGenericType(commandType);

      using (var scope = _container.CreateScope())
      {
        var commandHandler = scope.ServiceProvider.GetRequiredService(commandHandlerType);
        ((dynamic)commandHandler).Handle((dynamic)command);
      }
    }
  }

现在所有以前依赖于ICommandHandlerFactory 的代码都应该依赖于新的抽象ICommandDispatcher。只需记住在应用程序的组合根中定义类CommandDispatcher(组合根是允许依赖于 IOC 容器的代码的唯一模块)。

【问题讨论】:

  • 我认为迁移到 .Net Core 并远离 Castle Windsor(以及任何其他修复)可能应该分步完成。当事情已经足够复杂时,为什么还要复杂化。假设 Castle Windsor 适用于 .Net Standard/.Net Core
  • 不要。不要那样做。迁移本身就够复杂了。

标签: c# asp.net-core design-patterns dependency-injection ioc-container


【解决方案1】:

简单地说,工厂接管了 DI 容器。当您使用工厂时,工厂管理它创建的事物的生命周期。您可以使用 DI 容器来管理 工厂 的生命周期,但这就是它停止的地方。

如果不为您重写代码,您的问题实在是太宽泛了,无法合理回答。然而,本质上,工厂应该实现IDisposable。它应该更新它负责的东西,然后在Dispose 方法实现中处理它们。在Microsoft.Extensions.DependencyInjection 方面,您可以将工厂注册为单例(以便它可以在应用程序生命周期的整个范围内管理生命周期,因为此时它再次接管了 DI 容器的角色)。然后,您可以在任何需要的地方注入工厂,并使用它来创建您需要的内容。

【讨论】:

  • 所以你基本上是说拥有一个将对象创建委托给 ioc 容器的抽象工厂是没有意义的。也就是说,当您决定将对象创建委托给工厂时,您必须使用工厂本身处理对象创建和对象生命周期。
  • 是的。这就是工厂的意义所在。如果你可以从容器中取出实例,那么你就不需要工厂了。
  • 以IHttpClientFactory为例。之所以存在,是因为它需要在以名称/类型为键的并发字典中内部管理 HttpMessageHandler 实例。 DI 容器无法处理。如果你要求一个 HttpMessageHandler,它每次都会给你同一个
猜你喜欢
  • 1970-01-01
  • 2012-11-06
  • 2010-12-06
  • 2016-07-12
  • 1970-01-01
  • 1970-01-01
  • 2017-03-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多