依赖注入 and/vs 全局单例

Dependency Injection and/vs Global Singleton

我是依赖注入模式的新手。我喜欢这个想法,但很难将其应用到我的案例中。我有一个单例对象,我们称它为 X,我在程序的许多部分,在许多不同的 类 中经常需要它,有时在调用堆栈的深处。通常我会把它实现为一个全局可用的单例。这是如何在 DI 模式中实现的,特别是在 .NET Core DI 容器中?我知道我需要将 X 作为单例注册到 DI 容器,但是我如何才能访问它呢? DI 将使用引用 X 的构造函数实例化 类,这很好 - 但我需要 X 在调用层次结构的深处,在我自己的对象中,.NET Core 或 DI 容器对此一无所知,在创建的对象中使用 new 而不是由 DI 容器实例化。

我想我的问题是 – 全局单例模式 aligns/implemented by/replaced by/avoided 如何与 DI 模式?

嗯,“new 是胶水”(Link)。这意味着如果您有 new 个实例,它会粘附到您的实现中。您不能轻易地将它与不同的实现交换,例如用于测试的模拟。就像把乐高积木粘在一起一样。

如果您想使用适当的依赖注入(使用 container/framework 或不使用),您需要以一种不将组件粘合在一起而是注入它们的方式来构建程序。

每个 class 基本上都在层次结构级别 1。您需要记录器的实例吗?你注射它。您需要一个需要记录器的 class 实例?你注射它。您想测试您的日志记录机制吗?很简单,您只需注入符合记录器接口的内容即可登录到列表中,在测试结束时您可以检查列表并查看是否包含所有必需的日志。这是您可以自动化的事情(与使用正常的日志记录机制和手动检查日志文件相反)。

这意味着最后,你并没有真正的层次结构,因为每个 class 你只是注入了它们的依赖关系,它将是 container/framework 或你的控制代码来决定这对对象的实例化顺序意味着什么。


就设计模式而言,请允许我观察一下:即使是现在,您也不需要 单例。现在在你的程序中,如果你有一个普通的全局变量,它就会起作用。但我猜你读到全局变量是 "bad"。设计模式是 "good"。既然你需要一个全局变量,而单例提供了一个全局变量,那么当你可以使用 "good" 时,为什么要使用 "bad"?好吧,问题是,即使是单例,全局变量也不好。这是该模式的 缺点 ,您必须吞下一只蟾蜍才能使单例逻辑正常工作。在你的情况下,你不需要单例逻辑,但你喜欢蟾蜍的味道。所以你创建了一个单例。不要用设计模式这样做。仔细阅读它们并确保将它们用于预期目的,而不是因为您喜欢它们的副作用或因为使用设计模式感觉很好。

只是一个想法,也许我需要你的想法:

public static class DependencyResolver
{
    public static Func<IServiceProvider> GetServiceProvider;
}

然后在启动中:

public void Configure(IApplicationBuilder app, IServiceProvider serviceProvider)
{
    DependencyResolver.GetServiceProvider = () => { return serviceProvider; };
}

现在在任何契约中class:

DependencyResolver.GetServiceProvider().GetService<IService>();

这是一个简化示例,说明在没有单例的情况下如何工作。 本示例假设您的项目是按以下方式构建的:

  • 入口点是main
  • main 创建一个 class GuiCreator 的实例,然后调用方法 createAndRunGUI()
  • 其他一切都由该方法处理

因此您的简化代码如下所示:

// main
// ... (boilerplate)
container = new Container();
gui = new GuiCreator(container.getDatabase(), container.getLogger(), container.getOtherDependency());
gui.createAndRunGUI();
// ... (boilerplate)

// GuiCreator
public class GuiCreator {
    private IDatabase db;
    private ILogger log;
    private IOtherDependency other;
    
    public GuiCreator(IDatabase newdb, ILogger newlog, IOtherDependency newother) {
        db = newdb;
        log = newlog;
        other = newother;
    }
    
    public void createAndRunGUI() {
        // do stuff
    }
}

容器 class 是您实际定义将使用哪些实现的地方,而 GuiCreator 构造函数将接口作为参数。现在假设您选择的 ILogger 实现本身有一个依赖项,由其构造函数作为参数的接口定义。 Container 知道这一点并通过将 Logger 实例化为 new LoggerImplementation(getLoggerDependency()); 来相应地解决它。这适用于整个依赖链。

所以本质上:

  • 所有 classes 都将它们所依赖的接口实例保留为成员。
  • 这些成员在各自的构造函数中设置。
  • 当第一个对象被实例化时,整个依赖链就这样解决了。请注意,might/should 这里涉及一些延迟加载。
  • 访问容器方法以创建实例的唯一位置是在 main 和容器本身内部:
    • main 中使用的任何 class 都从 main 的容器实例接收其依赖项。
    • 任何 class 未在 main 中使用,而是仅用作依赖项,由容器实例化并从其中接收其依赖项。
    • 任何 class 既没有在 main 中使用也没有间接用作低于 main 中使用的 classes 的依赖项显然永远不会被实例化。
  • 因此,没有 class 实际上需要对容器的引用。事实上,没有 class 需要知道你的项目中甚至有一个容器。他们所知道的只是他们个人需要哪些接口。

容器可以由第三方提供 library/framework 或者您可以自己编写代码。通常,它会使用一些配置文件来确定哪些实现实际上应该用于各种接口。第三方容器通常会执行某种由“自动装配”实现的注释支持的代码分析,因此如果您使用 ready-made 工具,请确保您阅读了该部分的工作原理,因为它通常会影响您的生活在路上更容易。