ASP.NET: 为什么避免在工厂中使用 GetService?

ASP.NET: why avoid using GetService in a factory?

最近我找到了这篇文章:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1。它是关于 ASP.NET 核心应用程序中的依赖注入。里面有个推荐,我不是很懂:
“避免使用 GetService 注入在运行时解决依赖关系的工厂。”

问题是这种方法可能有用。假设我们必须在运行时创建 NSomething 实例。所以我们可以在 Client class 中注入一些工厂,它将使用该工厂创建 Something。但是,如果 Something 也有一些依赖项需要解决怎么办?这意味着我们需要在工厂中使用 GetService 来解析所有依赖项。但是以这种方式我们将使用这种我们应该避免的反模式。

所以我的问题是:为什么我们应该避免显式调用 GetService,我们如何实现类似的东西,但没有 GetService

希望我理解正确。

假设您有 10 个 类 实现了 ISomething。您可以像这样在您的 ServiceCollection 中注册它们:

services.AddTransient<ISomething, Something1>();
services.AddTransient<ISomething, Something2>();

通过这种方式,您的实现也可以具有依赖项,但它们是通过您的 DI 容器注入的。

如果您想在 class 中注入所有 ISomething 实现,只需通过构造函数注入它们。

public class SomethingConsumer
{
    public SomethingConsumer(IEnumerable<ISomthing> somethingList)
    {
    }
    ...
}

Microsoft 文档对此的建议并没有错,但由于缺乏大量上下文,因此过于简单化。我并不是真的责怪作家,因为可以写出关于这个主题的整本书(exactly what I did)。

有很多事情要考虑。要理解的第一个重要概念是 Composition Root。它是应用程序的一部分,其中注册依赖项并构建对象图。

另一个重要的概念是Service Locator anti-pattern。正如 anti-pattern 标签所暗示的那样,这是一个应该始终避免的模式。当您允许代码(位于组合根之外)解析 any 服务时,您正在应用此反模式。例如,当您将 Func<Type, object> 委托注入消费者时,或者当您将 IServiceProvider 注入消费者时,就会出现这种情况。它的 GetService 方法有那个委托。

另一方面,对于存在于内部组合根的代码,没有服务定位器的概念;这种反模式在那里不存在。那是因为 Composition Root 的主要功能是构造类型。它已经直接依赖于 DI 容器。 Composition Root 有望解析服务。

然而,重要的是要注意,具有特定委托与服务定位器不同。例如,如果你注入一个 Func<IMyService>,这是 而不是 一个服务定位器,因此,看起来不错。

但还有更多需要考虑的问题。通常,您应该以其消费者不必考虑实例数量的方式定义抽象。为了从抽象消费者的角度降低复杂性,这些消费者应该能够调用他们的抽象,就好像只有一个实例一样。

这并非在所有情况下都可行,但可以被视为一个很好的经验法则。但是当你应用这个经验法则时,工厂抽象(比如 Func<IMyService>)就没有什么空间了,因为它们会立即打破这个经验法则;消费者主动创建多个实例。

但即使消费者可能认为他们的依赖项是无状态的,依赖项的实现也可能不是。这仍然意味着必须在某个时候创建​​这些依赖项。如果您不允许他们的消费者这样做,谁可以创建它们?

这个问题的答案是:合成根。在大多数情况下,您应该将依赖项的创建从应用程序代码中移到组合根中。

以这个 HomeController 为例,它被注入了一个用于创建 IProductRepository 的工厂:

public class HomeController
{
    private Func<IProductRepository> repoFactory;

    public HomeController(Func<IProductRepository> repoFactory)
    {
        this.repoFactory = repoFactory;
    }

    public ViewResult Index()
    {
        var repository = this.repoFactory.Invoke();
        var products = repository.GetFeaturedProducts();
        return this.View(products);
    }
}

HomeController 的角度来看,有两个依赖关系需要考虑:Func<IProducerRepository>IProductRepository。这导致了复杂性。从HomeController的角度来看,更简单的解决方案如下:

public class HomeController
{
    private IProductRepository repository;

    public HomeController(IProductRepository repository)
    {
        this.repository = repository;
    }

    public ViewResult Index()
    {
        var products = this.repository.GetFeaturedProducts();
        return this.View(products);
    }
}

虽然这简化了 HomeController,但它转移了问题。但我们的想法是将这种复杂性转移到合成根中。

例如,在 Composition Root 中,您可以创建以下实现:

public class ProductRepositoryProxy : IProductRepository
{
    private Func<IProductRepository> repoFactory;

    public ProductRepositoryProxy(Func<IProductRepository> repoFactory)
    {
        this.repoFactory = repoFactory;
    }

    public IEnumerable<Product> GetFeaturedProducts()
    {
        var repository = this.repoFactory.Invoke();
        return repository.GetFeaturedProducts();
    }
}

ProductRepositoryProxy 包装原始 Func<IProductRepository> 并在每次调用时调用它。

ProductRepositoryProxy 假设任何 IProductRepository 实施都是有状态的,并且应该是短暂的。这很好,因为这些知识现在集中在 Composition Root 中。这简化了应用程序的其余部分,现在可以专注于业务决策,而不是依赖管理。

此描述是关于 滥用抽象工厂 的较长讨论的简短版本,可在 my book 的第 6.2 节中找到。