在接口层面解耦两个类是什么意思?

What does decoupling two classes at the interface level mean?

假设我们在包 A 中有 class A,在包 B 中有 class B。如果 class A 的对象引用了 class B,则称这两个 class 之间存在耦合。

为了解决耦合,建议在包A中定义一个接口,由包B中的class实现,这样classA的对象就可以引用包A中的接口。这通常是 "inversion of dependency".

中的示例

这是"decoupling two classes at the interface level"的例子吗。如果是,它如何消除 classes 之间的耦合并在两个 classes 耦合时保留相同的功能?

你描述的情况去掉了classA对classB的具体实现的依赖,换成了一个接口。现在 class A 可以接受任何实现接口类型的对象,而不是只接受 class B。设计保留了相同的功能,因为 class B 被用来实现那个接口.

假设 B 的功能是将日志写入某个数据库。 class B 依赖于 class DB 的功能,并为其他 classes.

的日志记录功能提供一些接口

Class A 需要 B 的日志记录功能,但并不关心日志写入的位置。它不关心 DB,但由于它依赖于 B,所以它也依赖于 DB。这不是很理想。

所以您可以做的是将 class B 拆分为两个 classes:描述日志功能的摘要 class L (并且不依赖于 DB),并且实现依赖于 DB

然后你可以将 class AB 解耦,因为现在 A 将只依赖于 LB 现在也依赖于 L,这就是它被称为依赖倒置的原因,因为 B 提供了 L 中提供的功能。

由于 A 现在仅依赖于精益 L,您可以轻松地将其与其他日志记录机制一起使用,而不依赖于 DB。例如。您可以创建一个简单的基于控制台的记录器,实现 L.

中定义的接口

但是因为现在 A 不依赖于 B 而是(在源代码中)只依赖于抽象接口 L 在 运行 时间它必须被设置使用 L 的某些特定实现(例如 B)。所以需要有人告诉 A 在 运行 时间内使用 B(或其他东西)。这称为 控制反转 ,因为在 A 决定使用 B 之前,但现在其他人(例如容器)告诉 A在 运行 时间内使用 B

让我们创建一个包含两个 classes AB.

的虚构示例

Class ApackageA:

package packageA;

import packageB.B;

public class A {
    private B myB;
    
    public A() {
        this.myB = new B();
    }
    
    public void doSomethingThatUsesB() {
        System.out.println("Doing things with myB");
        this.myB.doSomething();
    }
}

Class BpackageB:

package packageB;

public class B {
    public void doSomething() {
        System.out.println("B did something.");
    }
}

正如我们所见,A 取决于 B。如果没有 B,则无法使用 A。我们说 AB 紧密耦合。如果我们将来想用 BetterB 替换 B 怎么办?为此,我们在 packageA:

中创建一个接口 Inter
package packageA;

public interface Inter {
    public void doSomething();
}

为了利用这个界面,我们

  • import packageA.Inter;B implements InterB
  • A 中出现的所有 B 替换为 Inter

结果是 A 的修改版本:

package packageA;

public class A {
    private Inter myInter;
    
    public A() {
        this.myInter = ???; // What to do here?
    }
    
    public void doSomethingThatUsesInter() {
        System.out.println("Doing things with myInter");
        this.myInter.doSomething();
    }
}

我们已经可以看到从 AB 的依赖关系已经消失:不再需要 import packageB.B;。只有一个问题:我们无法实例化接口的实例。但是 Inversion of control 来拯救:不是在 A 的构造函数中实例化类型 Inter 的东西,构造函数将要求 implements Inter 作为参数的东西:

package packageA;

public class A {
    private Inter myInter;
    
    public A(Inter myInter) {
        this.myInter = myInter;
    }
    
    public void doSomethingThatUsesInter() {
        System.out.println("Doing things with myInter");
        this.myInter.doSomething();
    }
}

通过这种方法,我们现在可以在 A 中随意更改 Inter 的具体实现。假设我们写一个新的 class BetterB:

package packageB;

import packageA.Inter;

public class BetterB implements Inter {
    @Override
    public void doSomething() {
        System.out.println("BetterB did something.");
    }
}

现在我们可以用不同的 Inter 实现实例化 A

Inter b = new B();
A aWithB = new A(b);
aWithB.doSomethingThatUsesInter();

Inter betterB = new BetterB();
A aWithBetterB = new A(betterB);
aWithBetterB.doSomethingThatUsesInter();

而且我们不必在 A 内更改任何内容。代码现在解耦了,我们可以随意更改 Inter 的具体实现,只要满足 Inter 的契约即可。最值得注意的是,我们可以支持将来编写的代码并实现 Inter.


附录

我在2015年写了这个答案。虽然对这个答案总体上很满意,但我总觉得少了点什么,我想我终于知道是什么了。以下内容不是理解答案所必需的,但旨在激发对reader的兴趣,并为进一步的自学提供一些资源。

在文献中,这种方法被称为 Interface segregation principle and belongs to the SOLID-principles. There is a nice talk from uncle Bob on YouTube (the interesting bit is about 15 minutes long),展示了如何使用多态性和接口来让编译时依赖指向控制流(建议观众自行判断,Bob 叔叔会温和地咆哮约 Java)。在 return 中,这意味着高层实现不需要了解低层实现,当它们通过接口隔离时。因此,可以随意交换较低级别,如上所示。

这就是 DI(依赖注入)框架真正发挥作用的地方。

当您构建接口时,您实际上是在构建实施合同。您的调用服务将仅与合同交互,并承诺服务接口将始终提供其指定的方法。

例如...

您的 ServiceA 将围绕 ServiceB 的界面构建他们的逻辑,而不必担心 ServiceB 的幕后情况。

这允许您创建 ServiceB 的多个实现,而无需更改 ServiceA 中的任何逻辑。

举个例子

interface ServiceB { void doMethod() }

您可以在 ServiceA 中与 ServiceB 交互,而无需了解 ServiceB 的幕后情况。

class ServiceAImpl {

    private final ServiceB serviceB;
    
    public ServiceAImpl(ServiceBImpl serviceBImpl) {
        this.serviceB = serviceBImpl
    }

    public void doSomething() {
        serviceB.doMethod(); // calls ServiceB interface method.
    }

}

现在因为您已经使用 ServiceB 中指定的合同构建了 ServiceA,您可以根据需要更改实施。

您可以模拟服务,为不同的数据库创建不同的连接逻辑,创建不同的运行时逻辑。所有这些都可以改变,并且完全不会影响 ServiceAServiceB 的交互方式。

因此,IoC(控制反转)实现了松耦合。您现在拥有一个模块化且专注的代码库。