Java 在接口级别解耦两个类意味着什么?

Java 在接口级别解耦两个类意味着什么?,java,spring,inversion-of-control,decoupling,Java,Spring,Inversion Of Control,Decoupling,假设A包中有A类,B包中有B类。若类A的对象引用了类B,那个么这两个类之间就称为耦合 为了解决耦合问题,建议在包A中定义一个接口,该接口由包B中的类实现。然后,类A的对象可以引用包A中的接口。这通常是“依赖倒置”中的一个例子 这就是“在接口级别解耦两个类”的例子吗。如果是,当两个类耦合时,它如何消除类之间的耦合并保持相同的功能?您描述的情况消除了类A对类B的特定实现的依赖,并用接口替换它。现在,A类可以接受实现接口类型的任何对象,而不是只接受B类。设计保留了相同的功能,因为B类用于实现该接口。想

假设A包中有A类,B包中有B类。若类A的对象引用了类B,那个么这两个类之间就称为耦合

为了解决耦合问题,建议在包A中定义一个接口,该接口由包B中的类实现。然后,类A的对象可以引用包A中的接口。这通常是“依赖倒置”中的一个例子


这就是“在接口级别解耦两个类”的例子吗。如果是,当两个类耦合时,它如何消除类之间的耦合并保持相同的功能?

您描述的情况消除了类A对类B的特定实现的依赖,并用接口替换它。现在,A类可以接受实现接口类型的任何对象,而不是只接受B类。设计保留了相同的功能,因为B类用于实现该接口。

想象一下,
B
的功能是将日志写入某个数据库。类
B
取决于类
DB
的功能,并为其与其他类的日志功能提供一些接口

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

因此,您可以做的是将类
B
分为两个类:一个抽象类
L
,描述日志功能(不依赖于
DB
),一个实现依赖于
DB

然后可以将类
A
B
解耦,因为现在
A
将只依赖于
L
B
现在还依赖于
L
,这就是为什么它被称为依赖项反转,因为
B
提供了
L
中提供的功能

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


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

让我们创建一个虚拟的例子,包含两个类
A
B

包装
packageA
中的类别
A

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();
    }
}
package packageA;

public interface Inter {
    public void doSomething();
}
包装中的类
B

package packageB;

public class B {
    public void doSomething() {
        System.out.println("B did something.");
    }
}
正如我们所看到的,
A
依赖于
B
。如果没有
B
A
,则无法使用。我们说
A
B
紧密耦合。如果我们想在将来用一个更好的
B
来取代
B
,该怎么办?为此,我们在
packageA
中创建一个接口
Inter

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();
    }
}
package packageA;

public interface Inter {
    public void doSomething();
}
为了利用这个接口,我们

  • import packageA.Inter
    并让
    B在
    B
    中实现Inter
    ,以及
  • 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();
    }
}
我们已经可以看到从
A
B
的依赖关系消失了:导入包B.B不再需要。只有一个问题:我们不能实例化接口的实例。但总而言之:构造函数不会在
A
的构造函数中实例化
Inter
类型的东西,而是要求
实现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
的具体实现。假设我们编写一个新类
BetterB

package packageB;

import packageA.Inter;

public class BetterB implements Inter {
    @Override
    public void doSomething() {
        System.out.println("BetterB did something.");
    }
}
现在,我们可以使用不同的
内部
实现来实例化
A
s-实现:

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
的代码


Adendum

我在2015年写下了这个答案。虽然我对答案总体上感到满意,但我一直认为遗漏了什么,我想我终于知道了它是什么。以下内容不是理解答案所必需的,但旨在激发读者的兴趣,并为进一步的自我教育提供一些资源

在文献中,这种方法被称为并属于。有一个例子展示了如何使用多态性和接口来让编译时依赖关系指向控制流(建议查看者自行决定,Bob叔叔会温和地抱怨Java)。作为回报,这意味着高级实现在通过接口时不需要了解低级实现。因此,较低的级别可以随意交换,正如我们上面所示。

这就是DI(依赖注入)框架真正的亮点所在

当您构建接口时,实际上是在构建用于实现的契约。您的呼叫服务将仅与con进行交互