经过多年几乎无处不在的依赖注入的使用,我看到越来越多的帖子和谈话质疑它的价值。 有些人甚至到了反对的地步。 然而,大多数是基于大量的误解,半真半假和公然的谎言。
在本文中,我想回到DI的根源,描述一些相关功能并列出可用的框架。
像我5岁时解释一下
想象一个非常基础的类设计。 Car
类取决于CarEngine
类:
但是,我们了解到必须通过接口进行编程:
该实现可能如下所示:
interfaceEngine{
funstart():Boolean
}
classCarEngine:Engine{
overridefunstart()=...
}
classCar{
funstart(){
valengine=CarEngine()
when(engine.start()){
true->proceed()
false->throwNotStartedException()
}
...
}
但是,真正的类图现在看起来像这样:
为了单独测试Car
类,仅引入Engine
接口是不够的。 还应该防止Car
代码实例化新的CarEngine
实例:
classCar(privatevalengine:Engine){
funstart(){
when(engine.start()){
true->proceed()
false->throwEngineNotStartedException()
}
}
...
}
通过这种设计,可以根据上下文创建不同的Car
实例:
valcar=Car(CarEngine())
valmockEngine=mock(CarEngine::class.java)
valunitTestableCar=Car(mockEngine)
依赖注入的概念是将对象实例化从方法内部移动到外部,然后传递给后面的方法:仅此而已!
我想几乎没有人反对DI本身。 那么反对DI的趋势又如何发展呢?
我的疯狂猜测是,通常是由于缺乏对不同DI框架的了解而引起的。
类型学
DI框架可以根据不同的轴进行分组。 这里是其中的一些。
运行时与编译时
该博客的大多数读者都熟悉DI容器。 在启动时,它负责注入依赖项。
由于在运行时进行注入会增加启动应用程序所需的时间,因此它不适用于所有类型的应用程序:那些已多次启动且运行时间较短的应用程序。 在这种情况下,在编译时注入依赖关系更为重要。
Android应用程序就是这种情况。
构造函数vs设置方法vs现场注入
上面的设计显示了基于构造函数的注入:依赖项注入了构造函数。
但是,这不是唯一的方法。 备选方案包括:
-
基于二传手的注射
-
classCar(varengine:Engine)
这种方法不是一个好主意,因为没有理由在注入的对象生命周期中更改依赖项。
基于现场的注入
-
classCar{ @Injectprivatelateinitvarfoo:Engine }
这种方法甚至更糟,因为它不仅需要反射,而且还绕过安全检查 。
上述那些方法没有好处。 尽管某些DI框架以及某些测试框架允许使用这些框架,但应不惜一切代价避免使用它们。
显式与隐式接线
一些框架允许隐式依赖注入,也称为autowiring 。 为了满足依赖性,这样的框架将在上下文中搜索匹配的候选者。 如果他们找不到一个或多个,就会失败。
其他框架允许显式依赖项注入:在这种情况下,开发人员需要通过绑定注入的对象与依赖关系之间的关系来配置注入。
配置选项
每个框架都允许一种或多种配置方法。
让我们先谈谈房间里的大象。 Spring框架无处不在,以至于我已经看到它可以与DI互换使用。 绝对不是这样! 如上一节所示,DI不需要任何框架。 还有很多DI框架,而不仅仅是Spring,即使后者在服务器上拥有大量的DI Pie。
Spring框架允许最多数量的不同配置选项:
- XML格式
- 自注释类
- Java配置类
- Groovy
- Kotlin,通过Bean定义DSL
这是一个使用它的示例(从Spring博客复制):
beans{
bean<UserHandler>()
bean<Routes>()
bean<WebHandler>("webHandler"){
RouterFunctions.toWebHandler(
ref<Routes>().router(),
HandlerStrategies.builder().viewResolver(ref()).build()
)
}
bean("messageSource"){
ReloadableResourceBundleMessageSource().apply{
setBasename("messages")
setDefaultEncoding("UTF-8")
}
}
bean{
valprefix="classpath:/templates/"
valsuffix=".mustache"
valloader=MustacheResourceTemplateLoader(prefix,suffix)
MustacheViewResolver(Mustache.compiler().withLoader(loader)).apply{
setPrefix(prefix)
setSuffix(suffix)
}
}
profile("production"){
bean<Foo>()
}
}
虽然DI不能仅限于Spring框架,但后者也不能简化为前者! 它建立在DI之上,通过可重用的组件提供了整套功能。
摘要
以下是根据上述条件对框架及其功能进行的快速总结:
-
Spring框架
-
- Java服务器空间中的事实上的标准
- 运行
- 构造函数,二传手和现场注入
- 请参阅上面的配置选项
- 明确的接线和自动接线
上下文和依赖注入
-
- Java EE规范的一部分
- 运行
- 构造函数,二传手和现场注入
- 仅自我注释的课程
- 明确的布线和自动布线,有一定的趣味性
Google Guice
-
- 运行
- 构造函数,二传手和现场注入
- 仅自我注释的课程
- 仅自动接线
Pico容器
-
- 轻巧,顾名思义
我没有经验,以前也没看过
匕首2
- 轻巧,顾名思义
-
- Android上的事实上的标准
- 编译时间
- 构造函数,字段和方法注入,前两个倾向
- 自我注释类和外部类的组合
- 自动接线
结论
那么,为什么要使用DI? 问题应该是:为什么不使用DI? 它使您的代码更具模块化和更面向组件,从而使开发维护更容易 ,并且可测试性更高 。
在这篇文章中,我试图描述一组可用的选项。 可以对注入进行编码,或配置框架来进行注入。 可以选择运行时或编译时DI框架。 您可以选择轻巧的容器,也可以选择功能齐全的成熟容器。 肯定有一个与您的情况有关。