【问题标题】:Dependency Injection and/vs Global Singleton依赖注入和/vs 全局单例
【发布时间】:2019-05-06 23:38:04
【问题描述】:

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

我想我的问题是 – 全局单例模式如何与 DI 模式对齐/实现/替换/避免?

【问题讨论】:

  • 你能分享你的代码吗?
  • 感谢您的提问,但这个问题与特定的代码无关 - 而是关于模式如何/何时/为什么适用。
  • 好的,那我今天就去了解点新东西,等着有人回答。 :)
  • 我可以强烈推荐这个主题的 awesome book

标签: dependency-injection .net-core singleton


【解决方案1】:

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

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

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

这意味着最终,您并没有真正的层次结构,因为您拥有的每个类都只是注入了它们的依赖项,它将由容器/框架或您的控制代码来确定这对实例化顺序意味着什么的对象。


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

【讨论】:

  • 感谢您的评论 nvoigt。所以你的建议是,我不会在整个程序中使用 new 而是使用 DI 容器实例化?这意味着我会失去一些语法糖的便利,但这没关系。更有问题的是,在每个类中,我都必须保留对 DI 容器的引用,或者像上面 Khai Nguyen 建议的那样将其作为全局单例/变量。
  • 好吧,我不知道你的程序,所以我不能肯定,但关键是你现在用new 实例化的每个主要类都会被注入。因此,您不要用ServiceProvider.Getservice<> 替换new,而是例如在构造函数中注入该变量。除了你的main,没有人需要知道这个容器。其他人只需要在其构造函数中使用某些实例。
  • 所以这基本上归结为在调用堆栈中传递对 DI 容器的引用(无论如何 - 作为方法的参数,或作为类构造函数的参数),以便我拥有它到处。这样,我将 new 用于需要控制反转的小型/刚性/不重要类和 ServiceProvider.Getservice 。好吧,这似乎是一个可行的解决方案,即使有点过分。我会试试这个。
  • 啊,没有。也许可以快速解决您的单例问题。这与您拥有的new 无关。这与您使用的 all news 有关。您不会通过显式调用容器来替换它们。你重构你的代码,这样你就不需要一个新的实例,而是在你的构造函数中传递一个。也许您可以添加示例代码,我会将其更改为更适合 DI 的内容。
  • 显然,这只适用于具有依赖关系的类。如果你有 500 个整数并且你应该从中得到 250 个点 (x,y),你可以将 new 用于 Point 类。这里没有依赖关系。点类没有逻辑。
【解决方案2】:

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

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

然后在启动中:

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

现在在任何行为类别中:

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

【讨论】:

  • 感谢 Khai,这是我考虑过的方法之一。
【解决方案3】:

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

  • 入口点是 main
  • main 创建一个 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
    }
}

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

所以本质上:

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

容器可以由某些第三方库/框架提供,也可以由您自己编写代码。通常,它将使用一些配置文件来确定哪些实现实际上应该用于各种接口。第三方容器通常会执行某种由“自动装配”实现的注释支持的代码分析,所以如果你使用现成的工具,请确保你阅读了该部分的工作原理,因为它通常会让你的生活更轻松路。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-09-23
    • 1970-01-01
    • 2020-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多