【问题标题】:Proper DI or service locator anti-pattern?正确的 DI 或服务定位器反模式?
【发布时间】:2019-11-03 12:59:33
【问题描述】:

我正在开发一个 SDK,并在我的类及其依赖项中使用 DI(我认为是错误的)。我相信我已经混合了 DI 和服务定位器。我的问题是:

  • 知道在哪里构建我的 DI 容器。我没有可用作composition root 的运行时/入口点 - 并且不应该根据该帖子。
  • 服务生命周期。我创建 DI 容器的方式会导致创建多个单例对象,并且每个类都有自己的 DI 容器 - 我绝对想避免这种情况

这个DI via Builder equivalent to service locator anti-pattern? 似乎是我所追求的,但没有得到答复。

我用 .NET DI 定义我的容器:

public class ServiceProvider
{
        private Microsoft.Extensions.DependencyInjection.ServiceProvider serviceProvider;
        protected readonly IServiceCollection serviceCollection = new ServiceCollection();
        public ServiceProvider()
        {
            serviceCollection.AddScoped<IScopedService, ScopedService>();
            serviceCollection.AddSingleton<ISingletonService, SingletonService>();
        }
}

现在,当 ScopedService 依赖于 SingletonService 时,它​​是通过适当的构造函数注入完成的:

public ScopedService(ISingletonService singletonService)
{
    _singletonService = singletonService
}

但是消费(最终用户)类是通过服务定位器完成的,例如

private IScopedService _scopedService;
private IScopedService ScopedService
{
    get
    {
        return _scopedService ?? _scopedService = serviceProvider.GetService<IScopedService>();
    }
}

我的消费类都继承自一个基类,我相信每次实例化一个类时都会创建一个新的 ServiceProvider 实例。

    public abstract class ComponentBase
    {
        protected internal ServiceProvider serviceProvider = new ServiceProvider();
    }

问题:正在创建多个单例对象

            var singleton1 = consumingClass.serviceProvider.GetService<ISingletonService>();
            var singleton2 = consumingClass.serviceProvider.GetService<ISingletonService>();

Assert.AreEqual(singleton1, singleton2) // Test failure

我希望 Singleton 服务是同一个实例。我应该将基类中的 ServiceProvider 设为静态吗?这将解决问题,但似乎也朝着服务定位器反模式的方向发展。

我应该转向基于工厂的模式吗?我在服务提供者中注册所有消费类并且只有工厂知道 DI 容器?然后你会通过调用工厂的 getter 来获得一个类?

【问题讨论】:

  • 虽然 IMO 在某些情况下,类似于服务定位器的东西是两害相权取其轻的东西,但直接将容器注入到一个类中绝对是一种反模式,因为依赖关系变得不明确。该类可以从该容器请求anything。它创建了一个未定义的、开放式的依赖关系。 Here's an article(我的)展示了一个示例,其中服务定位器之类的东西可能会有所帮助,以及如何将其危害降到最低。
  • 我会编写每个类来依赖它应该依赖的内容。如果你这样做,几乎总有一种方法可以配置你的容器以提供这些依赖项。这通常很容易。如果这很难,那么这就是要解决的问题。您的课程将正确编写。解决问题所在,而不是让它泄漏到你的类中。
  • 谢谢@scotthannen,这是一篇非常有用的文章
  • 请不要通过破坏您的帖子为他人增加工作量。通过在 Stack Overflow 上发帖,您已根据CC BY-SA 3.0 license 授予 SO 分发该内容的不可撤销的权利。根据 SO 政策,任何破坏行为都将被撤销。如果您想了解更多关于删除帖子的信息,请访问How does deleting work?

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


【解决方案1】:

我建议不要在 SDK 中使用您自己的 IServiceProvider,类似于链接文章中所述。

而是为您的 SDK 公开一个可扩展点以插入到消费者的 DI 中

public static MySDKServiceCollectionExtensions {
    public static IServiceCollection AddMySDK(this IServiceCollection services) {
        services.AddScoped<IScopedService, ScopedService>();
        services.AddSingleton<ISingletonService, SingletonService>();

        //...additional services.

        return services
    }
}

如果您的消费组件需要访问这些服务,则通过构造函数注入使用显式依赖原则

public abstract class ComponentBase {        
    protected ISingletonService singleton;

    public ComponentBase(ISingletonService singleton) {
        this.singleton = singleton;
    }
}

在您的 SDK 中包含 IServiceProvider 被视为对您的 SDK 无关紧要的实现问题。

DI Container 只能从 Composition Root 引用。所有其他模块都不应引用容器。

消费项目/应用程序(最终用户)会将您的 SDK 添加到他们的组合根目录中

//...

IServiceCollection serviceCollection = new ServiceCollection();

//...Add SDK
serviceCollection.AddMySDK();

//Add my services.
serviceCollection.AddScoped<IConsumer, Consumer>()

//...

并根据需要相应地使用它

public class Consumer: IConsumer {
    protected internal IScopedService service;

    protected Consumer(IScopedService service) {
        this.service = service
    }

    private IScopedService ScopedService {
        get {
            return service;
        }
    }

    //...
}

【讨论】:

  • 我真的很喜欢这个答案。将我的服务添加到最终用户服务的扩展方法非常巧妙。不过我确实有一个问题 - 这样,我是否需要将我的最终用户/消费类注册为 ServiceCollection 中的服务?并用var component = services.GetService&lt;IConsumer&gt;();检索?
  • @JohnCurran 是的,如果您希望容器完成解决所有问题的所有繁重工作。
  • @JohnCurran 但这不应该是你关心的问题。他们将根据需要注册他们的课程
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-03-24
  • 2014-05-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-28
相关资源
最近更新 更多