我在把握依赖注入时遇到了问题(或者让我说它的好处)。因此,我决定编写两个简单的代码,一个不带DI,另一个不带DI。
所以我有一个A
类
public class A {
public void foo(){
B b = new B();
b.fooB();
}
}
如A
上方所示,取决于B
,B
,
public class B {
public void fooB(){
Log.e("s", "y");
}
}
我们可以像这样使用A
public void do(){
A a = new A();
a.foo();
}
但是,据说A
不应仅仅依赖于B
来初始化BService
,但是我们应该拥有一个在两个类之间具有某种契约的服务。例如,如果我错了,请告诉我
让我们有一个接口public interface BService {
void fooB();
}
B
然后DiB
变成public class DiB implements BService {
@Override
public void fooB(){
Log.e("s", "y");
}
}
A
然后DiA
变成public class DiA {
BService bService;
public DiA(BService bService){
this.bService = bService;
}
public void foo(){
bService.fooB();
}
}
A
我们可以像这样使用public void dIdo(){
BService service = new diB();
diA a = new diA(service);
a.foo();
}
B
所以我读到DI的好处是:
A
发生变化,那么fooB()
不应受到影响,我无法理解这一点,因为如果我将类B
中的fooB2()
更改为BService
,我将不得不在A
中更改覆盖方法,这又意味着我将不得不在类A
中更改它的覆盖方法这两个代码似乎都可以正常工作,我无法理解一个相对于另一个的好处,只是另一个更复杂。因此,请您进一步启发我了解这些简单的B
和{{1}}类所带来的好处。我没有得到什么?
答案 0 :(得分:3)
您不需要将其创建的接口归类为依赖项注入。此类使用依赖项注入:
public class A {
private final B b;
public A(B b) {
this.b = b;
}
public void foo(){
b.fooB();
}
}
不要想太多。当您不理解“依赖注入”时,它听起来像是一个复杂的概念,但是名称实际上完美而简洁地描述了这个概念。让我们分解一下:
依赖性:某些事物所依赖的事物
注射:将外部物体置于其他物体内部的行为
在上面的示例中,我们是否将我们依赖的东西从外部放入我们的类中?是的,我们是,所以我们正在使用依赖注入。我们的类是否实现接口无关紧要。
有充分的理由说明为什么要对接口实现类是一个好主意,但这与依赖注入有关。不要把这些东西弄糊涂了,也不要认为这是必须的。
解决可测试性:是的,在您的非依赖性注入版本中,我们可以测试A
和B
。我们无法与A
隔离地测试B
,但那又如何呢?谁说我们想要?这会给我们带来什么好处?
好吧,假设B
并不那么琐碎。假设B
从数据库中读取并返回一些值。我们不希望A
的单元测试依赖数据库,因为A
不在乎数据库,它只是在乎能够fooB
。不幸的是,如果A
是负责创建B
的人,那么我们就无法改变这种行为。它只能做一件事,在我们的生产代码中,我们需要它来创建一个B
来与数据库对话,所以这就是我们所坚持的。
但是,如果要注入依赖项,则可以在实际代码中执行此操作:
new A(new DatabaseB());
并在我们的测试中插入'fake' or a 'mock',其行为与实际正在与数据库进行通信的相似:
new A(mockB);
new A(fakeB);
这允许我们以两种不同的方式使用A
:有和没有数据库;用于生产代码和测试代码。它使我们可以灵活选择。
答案 1 :(得分:1)
解耦:它说如果B类发生变化,那么A不会受到影响,我无法理解,因为如果我将B类中的fooB()更改为fooB2(),我将不得不更改BService中的override方法反过来,这意味着我将不得不在A类中对其进行更改
我想一旦您了解了这一点,便会理解整个概念。
尝试考虑您作为系统不同组件之间的契约提供的接口。
通过使用方法BService
声明fooB()
,您是说遵守该合同的任何组件(例如,实现接口)都可以在其以自己的方式,只要它不违反合同即可。
在{strong>如何 A
的工作过程中,组件BService
不会引起人们的兴趣,因为A
足以知道要完成的工作。 / p>
然后,您将能够创建BService
的另一种实现,它可以完全不同地完成必要的工作。您可以重新配置IoC
,以将新的实现注入A
中。您尚未更改A
,但已更改其工作方式。
我们再举一个例子:
假设您有一个Repository
接口,可以通过某些字符串标识符存储/检索任何内容(为简单起见)。
interface Repository {
Object retrieve(String identifier);
void store(String identifier, Object content);
}
您可能有几个组件正在使用此存储库来处理某些数据:
class DocumentStorage {
private int seqNo = 1;
private Repository repository;
public void saveMyDocuments(Iterable<Document> documents) {
for (Document document : documents) {
repository.store("DocumentStorage" + ++seqNo, document);
}
}
}
还有
class RuntimeMetrics {
private Repository repository;
public void saveFreeMemoryAmount() {
repository.store("MEM", Runtime.getRuntime().freeMemory());
}
}
现在,这些组件不知道,他们只是知道存储库将如何保存文档。
您可以实现内存中存储库:
class InMemoryRepository implements Repository {
private final java.util.Map<Integer, Object> mem = new java.util.HashMap<>();
@Override
Object retrieve(Integer identifier) {
return mem.get(identifier);
}
@Override
void store(Integer identifier, Object content) {
mem.put(identifier, content);
}
}
并接受它。
现在,您可以在某个时间点确定文档太重要而无法存储在内存中,而必须将它们存储在文件或数据库或其他地方。
您正在根据DatabaseRepository
合同实施Repository
,重新配置DI容器和BOOM,您的文档现在位于数据库中。您在DocumentStorage
中没有进行任何更改,RuntimeMetrics
仍在使用InMemoryRepository
来管理其数据。
以类似的方式,您可以通过用伪造的实现替换DocumentStorage
而不是启动整个数据库服务器来测试Repository
。
这是DI
的主要优点。