依赖注入 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 工具,请确保您阅读了该部分的工作原理,因为它通常会影响您的生活在路上更容易。
我是依赖注入模式的新手。我喜欢这个想法,但很难将其应用到我的案例中。我有一个单例对象,我们称它为 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 工具,请确保您阅读了该部分的工作原理,因为它通常会影响您的生活在路上更容易。