【问题标题】:ASP.NET: why avoid using GetService in a factory?ASP.NET:为什么要避免在工厂中使用 GetService?
【发布时间】:2021-04-05 23:40:29
【问题描述】:

最近我发现了这篇文章:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1。这是关于 ASP.NET Core 应用程序中的依赖注入。那里有一个我不太明白的建议:
“避免注入在运行时使用 GetService 解析依赖项的工厂。”

问题是这种方法可能有用。假设我们必须在运行时创建 NSomething 实例。所以我们可以在 Client 类中注入一些工厂,它会使用该工厂来创建 Something。但是如果 Something 还有一些依赖项需要解决呢?这意味着我们需要在工厂中使用 GetService 来解决所有依赖关系。但是通过这种方式,我们会使用这种反模式,我们应该避免这种情况。

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

【问题讨论】:

  • 以 Autofac 为例,你可以注入一个Func<Dependency>。所以它注入 means to create 依赖项(而不是依赖项本身。这比 Service Locator 更可取。
  • 正如上面所说的DI is an alternative to static/global object access patterns. You may not be able to realize the benefits of DI if you mix it with static object access.你能把它们混合起来吗?当然。结局会好吗?可能不会。作为一般规则,服务定位器模式隐藏了依赖关系——而 DI 试图做相反的事情。
  • 这是一个一般性的建议,而不是必须的。如果你能避免这种情况,你应该这样做。否则,您通常仍然可以注入工厂。在某些涉及scoped dependencies/services 的情况下,您必须创建一个范围,而GetService 是您在创建的特定范围内(在运行时)获取和使用服务所需要的。
  • @mjwills 感谢您的快速回复,非常感谢。我明白了,所以如果我理解正确的话,Client 类可能依赖于返回 ISomething 的 lambda,并且在需要时我们只需调用该 lambda?

标签: c# asp.net-core dependency-injection factory


【解决方案1】:

Microsoft 文档中的建议并没有错,但它相当简单,因为它缺乏很多上下文。我真的不怪作者,因为可以写出关于这个主题的整本书(exactly what I did)。

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

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

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

然而,重要的是要注意,具有特定委托与服务定位器相同。例如,如果你注入一个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&lt;IProducerRepository&gt;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 中。

例如,在 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&lt;IProductRepository&gt; 并在每次调用时调用它。

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

此描述是关于Abuse of Abstract Factories的较长讨论的简短版本,可在my book 的第6.2 节中找到。

【讨论】:

  • 如果 OP 使用 Autofac(例如),则不需要有两个单独的 IProductRepository 实现。构建一个,Autofac 也将自动支持Func 场景。此外,在您在此处显示的特定场景中,Lazy 可能是比Func 更好的解决方案(因为您只需要 0 或 1 个存储库实例)。甚至动作注入可能更可取。 除此之外,很好的答案。
  • @mjwills,在这方面还有很多话要说。我只是试图说明为什么通常应该避免在 Composition Root 之外使用这些工厂(但 Lazy 也是如此)。但是,很难在一个 SO 答案中总结这些知识。如果我没有成功,我深表歉意。
  • 很好的答案 - 谢谢你把它放在一起。
【解决方案2】:

希望我理解正确。

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

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

这样你的实现也可以有依赖,但它们是通过你的 DI 容器注入的。

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

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

【讨论】:

  • 不,我认为这不是 OP 想要的。他想要创建的实例数量是灵活的、动态的。他可能希望在运行时创建动态数量的相同服务类型的实例。
  • 啊,我明白了。不,那不是正确的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-06-25
  • 1970-01-01
  • 2018-03-01
  • 1970-01-01
  • 2013-08-19
  • 2010-12-23
  • 1970-01-01
相关资源
最近更新 更多