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

时间:2015-06-11 14:51:23

标签: java spring inversion-of-control decoupling

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

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

这是“在接口级别解耦两个类”的示例。如果是,那么它如何消除类之间的耦合并在两个类耦合时保留相同的功能?

4 个答案:

答案 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 AInter替换所有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

此时,我们已经看到从Bimport 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

通过这种方法,我们现在可以随意更改ABetterB的具体实施。假设我们写了一个新的类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

然后,您可以将课程AB分开,因为现在A仅依赖于LB现在也依赖于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,因此您可以根据需要更改实现。

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

因此,通过IoC(控制反转)可以实现松散耦合。您现在拥有了一个模块化且重点突出的代码库。