已经发布了几个关于依赖注入的具体问题,例如何时使用它以及它有什么框架,
什么是依赖注入,何时/为什么应该或不应该使用它?
已经发布了几个关于依赖注入的具体问题,例如何时使用它以及它有什么框架,
什么是依赖注入,何时/为什么应该或不应该使用它?
当前回答
流行的答案毫无用处,因为它们以一种无用的方式定义依赖注入。让我们同意,“依赖性”是指我们的对象X所需要的一些预先存在的其他对象。但当我们说
$foo = Foo->new($bar);
我们只调用将参数传递到构造函数中。自从构造器被发明以来,我们一直在定期这样做。
“依赖注入”被认为是“控制反转”的一种类型,这意味着某些逻辑被从调用者中取出。当调用者传入参数时,情况并非如此,因此如果是DI,DI就不会意味着控制反转。
DI意味着在调用者和构造函数之间有一个中间层来管理依赖关系。Makefile是依赖注入的一个简单示例。“调用者”是在命令行上键入“make bar”的人,“构造函数”是编译器。Makefile指定bar依赖于foo,并执行
gcc -c foo.cpp; gcc -c bar.cpp
在执行
gcc foo.o bar.o -o bar
键入“makebar”的人不需要知道bar依赖于foo。在“makebar”和gcc之间注入了依赖关系。
中间层的主要目的不仅仅是将依赖项传递给构造函数,而是在一个地方列出所有依赖项,并向编码器隐藏它们(而不是让编码器提供它们)。
通常,中间层为构造的对象提供工厂,这些对象必须提供每个请求的对象类型都必须满足的角色。这是因为通过拥有一个隐藏构建细节的中间层,您已经受到了工厂施加的抽象惩罚,所以您不妨使用工厂。
其他回答
依赖注入是将依赖传递给其他对象或框架(依赖注入器)。
依赖注入使测试更容易。注入可以通过构造函数完成。
SomeClass()的构造函数如下:
public SomeClass() {
myObject = Factory.getObject();
}
问题:如果myObject涉及诸如磁盘访问或网络访问之类的复杂任务,则很难在SomeClass()上进行单元测试。程序员必须模拟myObject,并可能拦截工厂调用。
替代解决方案:
将myObject作为参数传入构造函数
public SomeClass (MyClass myObject) {
this.myObject = myObject;
}
myObject可以直接传递,这使得测试更容易。
一种常见的替代方法是定义一个不做任何事情的构造函数。依赖注入可以通过setter完成。(h/t@MikeVella)。Martin Fowler记录了第三种选择(h/t@MarcDix),其中类显式地实现了程序员希望注入的依赖项的接口。
在没有依赖注入的情况下,很难在单元测试中隔离组件。
2013年,当我写下这个答案时,这是谷歌测试博客的一个主要主题。这对我来说仍然是最大的优势,因为程序员在运行时设计中并不总是需要额外的灵活性(例如,服务定位器或类似模式)。程序员通常需要在测试期间隔离类。
依赖注入(DI)的全部目的是保持应用程序源代码干净和稳定:
清除依赖项初始化代码无论使用的依赖关系如何稳定
实际上,每个设计模式都将关注点分开,以使将来的更改影响最小的文件。
DI的特定域是依赖配置和初始化的委托。
示例:带有shell脚本的DI
如果您偶尔在Java之外工作,请回想一下源代码在许多脚本语言(Shell、Tcl等,甚至在Python中被误用)中的使用情况。
考虑简单的dependent.sh脚本:
#!/bin/sh
# Dependent
touch "one.txt" "two.txt"
archive_files "one.txt" "two.txt"
脚本是依赖的:它无法单独成功执行(未定义存档文件)。
可以在archive_files_ip.sh实现脚本中定义archive_files(在本例中使用zip):
#!/bin/sh
# Dependency
function archive_files {
zip files.zip "$@"
}
您可以使用一个injector.sh“container”来包装这两个“components”,而不是直接在依赖脚本中源代码化实现脚本:
#!/bin/sh
# Injector
source ./archive_files_zip.sh
source ./dependent.sh
archive_files依赖项刚刚注入到依赖脚本中。
您可能已经注入了使用tar或xz实现archive_files的依赖项。
示例:删除DI
如果dependent.sh脚本直接使用依赖项,则该方法将被称为依赖项查找(与依赖项注入相反):
#!/bin/sh
# Dependent
# dependency look-up
source ./archive_files_zip.sh
touch "one.txt" "two.txt"
archive_files "one.txt" "two.txt"
现在的问题是依赖的“组件”必须自己执行初始化。
“组件”的源代码既不干净也不稳定,因为依赖项初始化中的每一次更改都需要“组件”源代码文件的新版本。
最后一句话
DI并不像Java框架那样被广泛强调和普及。
但这是一种通用的方法,可以解决以下问题:
应用程序开发(单一源代码发布生命周期)应用程序部署(具有独立生命周期的多个目标环境)
仅将配置与依赖项查找一起使用没有帮助,因为每个依赖项的配置参数数量(例如,新的身份验证类型)以及支持的依赖项类型数量(例如新的数据库类型)可能会发生变化。
这是我见过的关于依赖注入和依赖注入容器的最简单的解释:
无依赖注入
应用程序需要Foo(例如控制器),因此:应用程序创建Foo应用程序调用FooFoo需要Bar(例如服务),因此:Foo创建BarFoo调用Bar酒吧需要Bim(服务、存储库,…),因此:条形图创建Bim酒吧有点事
使用依赖注入
应用程序需要Foo,需要Bar,需要Bim,因此:应用程序创建Bim应用程序创建Bar并赋予它Bim应用程序创建Foo并给它Bar应用程序调用FooFoo调用Bar酒吧有点事
使用依赖注入容器
应用程序需要Foo,因此:应用程序从容器中获取Foo,因此:容器创建Bim容器创建Bar并赋予它Bim容器创建Foo并给它Bar应用程序调用FooFoo调用Bar酒吧有点事
依赖注入和依赖注入容器是不同的:
依赖注入是一种编写更好代码的方法DI容器是帮助注入依赖项的工具
您不需要容器来执行依赖注入。然而,容器可以帮助您。
公认的答案是一个好答案——但我想补充一点,DI非常像代码中避免硬编码常量的经典做法。
当您使用诸如数据库名称之类的常量时,您可以将其从代码内部快速移动到某个配置文件,并将包含该值的变量传递到需要它的位置。这样做的原因是,这些常量通常比代码的其他部分更频繁地更改。例如,如果您想在测试数据库中测试代码。
在面向对象编程的世界中,DI与此类似。那里的值而不是常量文字是整个对象-但是将创建它们的代码从类代码中移出的原因是相似的-对象的更改比使用它们的代码更频繁。一个重要的情况是需要进行这样的改变,那就是测试。
依赖注入(DI)是设计模式中的一种,它使用了OOP的基本特性——一个对象与另一个对象之间的关系。虽然继承继承一个对象以实现更复杂和更具体的另一个对象,但关系或关联只需使用属性从一个对象创建指向另一对象的指针。DI的功能与OOP的其他特性相结合,如接口和隐藏代码。假设图书馆里有一个客户(订阅者),为了简单起见,他只能借一本书。
书本界面:
package com.deepam.hidden;
public interface BookInterface {
public BookInterface setHeight(int height);
public BookInterface setPages(int pages);
public int getHeight();
public int getPages();
public String toString();
}
接下来我们可以有很多种书;其中一种类型是虚构:
package com.deepam.hidden;
public class FictionBook implements BookInterface {
int height = 0; // height in cm
int pages = 0; // number of pages
/** constructor */
public FictionBook() {
// TODO Auto-generated constructor stub
}
@Override
public FictionBook setHeight(int height) {
this.height = height;
return this;
}
@Override
public FictionBook setPages(int pages) {
this.pages = pages;
return this;
}
@Override
public int getHeight() {
// TODO Auto-generated method stub
return height;
}
@Override
public int getPages() {
// TODO Auto-generated method stub
return pages;
}
@Override
public String toString(){
return ("height: " + height + ", " + "pages: " + pages);
}
}
现在,用户可以与图书建立关联:
package com.deepam.hidden;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class Subscriber {
BookInterface book;
/** constructor*/
public Subscriber() {
// TODO Auto-generated constructor stub
}
// injection I
public void setBook(BookInterface book) {
this.book = book;
}
// injection II
public BookInterface setBook(String bookName) {
try {
Class<?> cl = Class.forName(bookName);
Constructor<?> constructor = cl.getConstructor(); // use it for parameters in constructor
BookInterface book = (BookInterface) constructor.newInstance();
//book = (BookInterface) Class.forName(bookName).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return book;
}
public BookInterface getBook() {
return book;
}
public static void main(String[] args) {
}
}
这三个类都可以隐藏起来,以便实现自己的功能。现在我们可以将此代码用于DI:
package com.deepam.implement;
import com.deepam.hidden.Subscriber;
import com.deepam.hidden.FictionBook;
public class CallHiddenImplBook {
public CallHiddenImplBook() {
// TODO Auto-generated constructor stub
}
public void doIt() {
Subscriber ab = new Subscriber();
// injection I
FictionBook bookI = new FictionBook();
bookI.setHeight(30); // cm
bookI.setPages(250);
ab.setBook(bookI); // inject
System.out.println("injection I " + ab.getBook().toString());
// injection II
FictionBook bookII = ((FictionBook) ab.setBook("com.deepam.hidden.FictionBook")).setHeight(5).setPages(108); // inject and set
System.out.println("injection II " + ab.getBook().toString());
}
public static void main(String[] args) {
CallHiddenImplBook kh = new CallHiddenImplBook();
kh.doIt();
}
}
如何使用依赖注入有许多不同的方法。可以将它与Singleton等结合起来,但基本上它只是通过在另一个对象内创建对象类型的属性来实现的关联。它的有用性是唯一的,也是唯一的特点,我们应该反复编写的代码总是为我们准备好并做好准备。这就是为什么DI如此紧密地与控制反转(IoC)绑定,这意味着我们的程序将控制传递给另一个正在运行的模块,该模块将bean注入到我们的代码中。(可以被注入的每个对象都可以被签名或被认为是一个Bean。)例如,在Spring中,它是通过创建和初始化ApplicationContext容器来完成的,这对我们来说很有用。我们只需在代码中创建Context并调用初始化Bean。此时注射已自动完成。