假设我们在包A中有A类,在包B中有B类。如果类A的对象引用了类B,则说这两个类之间有耦合。
为解决耦合问题,建议在包A中定义一个接口,该接口由包B中的类实现。然后,类A的对象可以引用包A中的接口。这通常是“依赖倒置”的一个例子。
这是“在接口级别解耦两个类”的示例。如果是,那么它如何消除类之间的耦合并在两个类耦合时保留相同的功能?
答案 0 :(得分:38)
让我们创造一个虚构的例子。
包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();
}
}
包B
中的packageB
类:
package packageB;
public class B {
public void doSomething() {
System.out.println("B did something.");
}
}
如我们所见,A
取决于B
。如果没有B
,则无法使用A
。但是如果我们希望将来用B
替换BetterB
怎么办?为此,我们在Inter
:
packageA
package packageA;
public interface Inter {
public void doSomething();
}
要使用此界面,我们import packageA.Inter;
并B implements Inter
B
,并B
A
内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;
的依赖关系已经消失:不再需要Inter
。只有一个问题:我们无法实例化接口的实例。但是Inversion of control拯救了:而不是实例化类型为A
wihtin 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();
}
}
作为参数的东西:
Inter
通过这种方法,我们现在可以随意更改A
内BetterB
的具体实施。假设我们写了一个新的类package packageB;
import packageA.Inter;
public class BetterB implements Inter {
@Override
public void doSomething() {
System.out.println("BetterB did something.");
}
}
:
A
现在我们可以使用不同的Inter
实现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
的具体实现。最值得注意的是,我们可以支持将来编写的代码并实现CREATE view vet_visits_over_first_30_days AS
(SELECT veterinarian_session.animal_id AS "animal", veterinarian_account.enrollment_date AS "Enrolled", veterinarian_sessionactivity.created_date AS "Admit Date", veterinarian_sessionactivity.detail AS "Reason"
FROM veterinarian_account, veterinarian_sessionactivity, veterinarian_session
WHERE veterinarian_sessionactivity.created_date BETWEEN veterinarian_account.enrollment_date AND veterinarian_account.enrollment_date + '30 days'::INTERVAL AND veterinarian_sessionactivity.activity_sub_id=52 AND veterinarian_sessionactivity.session_id = veterinarian_session.id AND veterinarian_session.animal_id=veterinarian_account.id
ORDER BY veterinarian_session.animal_id);
。
<强> Adendum 强>
两年前我写了这个答案。虽然总体上对答案感到满意,但我一直认为缺少了某些东西,我想我终于知道它是什么了。以下内容对于理解答案没有必要,但旨在引起读者的兴趣,并为进一步的自我教育提供一些资源。
在文献中,这种方法称为Interface segregation principle,属于SOLID-principles。有一个nice talk from uncle Bob on YouTube (the interesting bit is about 15 minutes long)显示了如何使用多态和接口让编译时依赖点指向控制流(建议查看者自行决定,鲍勃叔叔会轻微地咆哮Java)。反过来,这意味着高级实现在通过接口进行segretaget时不需要了解更低级别的实现。因此,可以随意交换较低的水平,如上所示。
答案 1 :(得分:4)
想象一下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
使用{{} 1}}在运行时。
答案 2 :(得分:3)
您描述的情况消除了类A对类B的特定实现的依赖性,并将其替换为接口。现在,类A可以接受任何实现接口的类型的对象,而不是仅接受类B.该设计保留相同的功能,因为B类用于实现该接口。
答案 3 :(得分:0)
这是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.
}
}
现在,由于您已经使用ServiceA
中指定的合同构建了ServiceB
,因此您可以根据需要更改实现。
您可以模拟服务,为不同的数据库创建不同的连接逻辑,创建不同的运行时逻辑。所有这些都可以更改,并且完全不会影响ServiceA
与ServiceB
的交互方式。
因此,通过IoC(控制反转)可以实现松散耦合。您现在拥有了一个模块化且重点突出的代码库。