在接口层面解耦两个类是什么意思?
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 A
与 B
解耦,因为现在 A
将只依赖于 L
。 B
现在也依赖于 L
,这就是它被称为依赖倒置的原因,因为 B
提供了 L
中提供的功能。
由于 A
现在仅依赖于精益 L
,您可以轻松地将其与其他日志记录机制一起使用,而不依赖于 DB
。例如。您可以创建一个简单的基于控制台的记录器,实现 L
.
中定义的接口
但是因为现在 A
不依赖于 B
而是(在源代码中)只依赖于抽象接口 L
在 运行 时间它必须被设置使用 L
的某些特定实现(例如 B
)。所以需要有人告诉 A
在 运行 时间内使用 B
(或其他东西)。这称为 控制反转 ,因为在 A
决定使用 B
之前,但现在其他人(例如容器)告诉 A
在 运行 时间内使用 B
。
让我们创建一个包含两个 classes A
和 B
.
的虚构示例
Class A
包 packageA
:
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 B
包 packageB
:
package packageB;
public class B {
public void doSomething() {
System.out.println("B did something.");
}
}
正如我们所见,A
取决于 B
。如果没有 B
,则无法使用 A
。我们说 A
与 B
紧密耦合。如果我们将来想用 BetterB
替换 B
怎么办?为此,我们在 packageA
:
中创建一个接口 Inter
package packageA;
public interface Inter {
public void doSomething();
}
为了利用这个界面,我们
import packageA.Inter;
让 B implements Inter
在 B
和
- 将
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
的依赖关系已经消失:不再需要 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
,您可以根据需要更改实施。
您可以模拟服务,为不同的数据库创建不同的连接逻辑,创建不同的运行时逻辑。所有这些都可以改变,并且完全不会影响 ServiceA
与 ServiceB
的交互方式。
因此,IoC(控制反转)实现了松耦合。您现在拥有一个模块化且专注的代码库。
假设我们在包 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 A
与 B
解耦,因为现在 A
将只依赖于 L
。 B
现在也依赖于 L
,这就是它被称为依赖倒置的原因,因为 B
提供了 L
中提供的功能。
由于 A
现在仅依赖于精益 L
,您可以轻松地将其与其他日志记录机制一起使用,而不依赖于 DB
。例如。您可以创建一个简单的基于控制台的记录器,实现 L
.
但是因为现在 A
不依赖于 B
而是(在源代码中)只依赖于抽象接口 L
在 运行 时间它必须被设置使用 L
的某些特定实现(例如 B
)。所以需要有人告诉 A
在 运行 时间内使用 B
(或其他东西)。这称为 控制反转 ,因为在 A
决定使用 B
之前,但现在其他人(例如容器)告诉 A
在 运行 时间内使用 B
。
让我们创建一个包含两个 classes A
和 B
.
Class A
包 packageA
:
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 B
包 packageB
:
package packageB;
public class B {
public void doSomething() {
System.out.println("B did something.");
}
}
正如我们所见,A
取决于 B
。如果没有 B
,则无法使用 A
。我们说 A
与 B
紧密耦合。如果我们将来想用 BetterB
替换 B
怎么办?为此,我们在 packageA
:
Inter
package packageA;
public interface Inter {
public void doSomething();
}
为了利用这个界面,我们
import packageA.Inter;
让B implements Inter
在B
和- 将
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
的依赖关系已经消失:不再需要 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
,您可以根据需要更改实施。
您可以模拟服务,为不同的数据库创建不同的连接逻辑,创建不同的运行时逻辑。所有这些都可以改变,并且完全不会影响 ServiceA
与 ServiceB
的交互方式。
因此,IoC(控制反转)实现了松耦合。您现在拥有一个模块化且专注的代码库。