在两个简单类的上下文中掌握依赖注入

时间:2019-01-23 11:06:29

标签: java dependency-injection

我在把握依赖注入时遇到了问题(或者让我说它的好处)。因此,我决定编写两个简单的代码,一个不带DI,另一个不带DI。

所以我有一个A

public class A {
    public void foo(){
        B b = new B();
        b.fooB();
    }
}

A上方所示,取决于BB

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的好处是:

  1. 可测试的代码:因为我实际上可以在JUnit中测试这两个代码(我不想在这里发布测试,以避免冗长的问题)
  2. 解耦:它说如果类A发生变化,那么fooB()不应受到影响,我无法理解这一点,因为如果我将类B中的fooB2()更改为BService,我将不得不在A中更改覆盖方法,这又意味着我将不得不在类A中更改它的覆盖方法

这两个代码似乎都可以正常工作,我无法理解一个相对于另一个的好处,只是另一个更复杂。因此,请您进一步启发我了解这些简单的B和{{1}}类所带来的好处。我没有得到什么?

2 个答案:

答案 0 :(得分:3)

您不需要将其创建的接口归类为依赖项注入。此类使用依赖项注入:

public class A {
    private final B b;

    public A(B b) {
        this.b = b;
    }

    public void foo(){
        b.fooB();
    }
}

不要想太多。当您不理解“依赖注入”时,它听起来像是一个复杂的概念,但是名称实际上完美而简洁地描述了这个概念。让我们分解一下:

依赖性:某些事物所依赖的事物
   注射:将外部物体置于其他物体内部的行为

在上面的示例中,我们是否将我们依赖的东西从外部放入我们的类中?是的,我们是,所以我们正在使用依赖注入。我们的类是否实现接口无关紧要。

有充分的理由说明为什么要对接口实现类是一个好主意,但这与依赖注入有关。不要把这些东西弄糊涂了,也不要认为这是必须的。


解决可测试性:是的,在您的非依赖性注入版本中,我们可以测试AB。我们无法与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的主要优点。