在两个简单类的背景下理解依赖注入

4

我一直在理解依赖注入(或者说是它的好处)方面存在问题。因此,我决定编写两个简单的代码片段,一个没有使用 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();
}

我阅读了关于DI的好处:

  1. 可测试的代码:因为我可以在JUnit中测试两个代码(我不想在此处发布测试以避免冗长的问题)
  2. 解耦:据说如果类B更改,则不应影响A,但我无法理解,因为如果我在类B中将fooB()更改为fooB2(),那么我将不得不更改BService中的覆盖方法,这反过来意味着我将不得不在类A中进行更改

这两个代码似乎都很好用,我无法理解其中一个胜过另一个的好处,只是另一个更加复杂。所以,请您在这个简单的AB类的上下文中更详细地解释一下好处。我没有理解到什么?

2个回答

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());

我们需要在测试中注入一个'假'对象或者 '模拟'对象,使其表现得像是在与数据库交互,但实际上并没有真正地进行操作:

new A(mockB);
new A(fakeB);

这使我们能够以两种不同的方式使用A:带有或不带有数据库;用于生产代码和测试代码。这给了我们选择的灵活性。

1
解耦:有人说如果B类改变,那么A不应该受到影响,但我无法理解,因为如果我将B类中的fooB()更改为fooB2(),那么我将不得不更改BService中的覆盖方法,这反过来意味着我必须在A类中进行更改。我想一旦你理解了这一点,你就能理解整个概念了。
试着把你提供的接口看作是系统中不同组件之间的合同。通过声明具有方法fooB()的BService,你正在表明任何遵守此合同(例如实现接口)的组件都能以自己的方式完成声明的工作,只要它不违反合同。组件A不应该对BService如何完成其工作感兴趣,因为对于A来说,知道工作将被完成就足够了。
然后,您将能够创建另一个实现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);
         }
    }
}

And

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);
    }
}

并且接受这个事实。

在某个时间点,您可以决定文档太重要了,不能存储在内存中,必须存储在文件、数据库或其他地方。

您正在根据Repository合同实现DatabaseRepository,重新配置DI容器,BOOM,您的文档现在存在于数据库中。您没有改变DocumentStorage中的任何内容,RuntimeMetrics仍然使用InMemoryRepository来管理其数据。

以类似的方式,您可以通过使用假实现替换Repository而不是启动整个数据库服务器来测试DocumentStorage

这是DI的主要好处。


我觉得你的大部分回答都与设计接口有关,虽然这本身是很好的,但并不是依赖注入的必要组成部分。 - Michael

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接