“按接口编程”是什么意思?

920
我在几次提到过这个,但不清楚它是什么意思。你什么时候和为什么要这样做?
我知道接口是做什么的,但因为我不太清楚这一点,所以我认为我可能没有正确地使用它们。
这只是为了让你能够执行以下操作吗?
IInterface classRef = new ObjectWhatever()

你可以使用任何实现了 IInterface 接口的类?那么什么情况下需要这样做呢?我所能想到的唯一情形是,当你有一个方法并且不确定将传递哪个对象,除非它实现了 IInterface 接口。我认为这种情况并不经常出现。

另外,你如何编写一个接收实现某个接口的对象作为参数的方法呢?这是可能的吗?


4
如果你记得的话,并且你的程序需要优化,在编译之前,你可能希望交换接口声明与实际实现。因为使用接口会增加间接性,从而影响性能。但是,建议将代码编写为基于接口编程的形式进行分发。 - Ande Turner
25
@Ande Turner:那是很糟糕的建议。1)“你的程序需要是最优的”不是替换接口的好理由!然后你说“通过接口编写代码…”,所以你建议在满足要求(1)的情况下发布次优代码吗?!?Translated: @Ande Turner: 那是差劲的建议。1)“你的程序需要是最优的”不是替换接口的好理由!然后你说“通过接口编写代码…”,所以你建议在满足需求(1)的情况下发布次优代码吗?!? - Mitch Wheat
82
这里大部分答案都不太正确。它根本不意味着“使用interface关键字”。接口是使用某物的规范,与合同(请查阅)相同。除此之外的是实现,即如何履行该合同。只针对方法/类型的保证进行编程,以便在方法/类型以遵守合同的方式发生变化时,不会破坏使用它的代码。 - jyoungdev
2
@apollodude217,这实际上是整个页面上最好的答案。至少对于标题中的问题来说是这样,因为这里至少有3个非常不同的问题... - Andrew Spencer
7
这类问题的根本问题在于它假设“按照接口编程”意味着“将所有内容都封装在抽象接口中”,如果你考虑到这个术语早于Java风格的抽象接口概念,那么这种想法就是愚蠢的。 - Jonathan Allen
显示剩余9条评论
33个回答

9

如果您使用Java编程,JDBC是一个很好的例子。 JDBC定义了一组接口,但不涉及实现细节。 您的应用程序可以根据这组接口编写。 理论上,您选择某个JDBC驱动程序,您的应用程序将正常工作。 如果您发现有更快或“更好”或更便宜的JDBC驱动程序,或出于任何原因,您可以重新配置属性文件,而无需在应用程序中进行任何更改,您的应用程序仍将正常工作。


它不仅在更好的驱动程序可用时有用,而且使完全更改数据库供应商成为可能。 - Ken Liu
3
JDBC很糟糕,需要替换。请提供另一个例子。 - Joshua
JDBC 不好,但与接口与实现或抽象级别无关。因此,为了说明所讨论的概念,它非常完美。 - Erwin Smout

8
编程到接口是很棒的,它促进了松耦合。正如@lassevk所提到的,控制反转是其很好的应用。
此外,还要了解SOLID原则。这里有一个视频系列 它会先讲解一个硬编码(强耦合示例),然后介绍接口,最后进展到IoC / DI工具(NInject)。

8
我是一个晚来者,但我想在这里提到,“面向接口编程,而不是实现细节”的这条原则在GoF(四人帮)的《设计模式》一书中有很好的讨论。
书中在第18页上写道:
“面向接口编程,而不是实现细节”
不要将变量声明为特定具体类的实例。相反,只承诺由抽象类定义的接口。您会发现这是本书设计模式的一个共同主题。
在此之前,它开始于以下内容:
“仅以抽象类定义的接口操纵对象有两个好处:”
客户端只需使用他们期望的接口,只要对象遵循该接口,客户端就不知道他们使用的对象的具体类型。
客户端不知道实现这些对象的类。客户端只知道定义接口的抽象类(们)。
换句话说,不要编写具有鸭子的quack()方法,然后具有狗的bark()方法,因为它们对于类(或子类)的特定实现过于具体。相反,使用足够通用以在基类中使用的名称编写方法,例如giveSound()或move(),以便它们可以用于鸭子、狗甚至汽车,然后您的类的客户端只需说.giveSound()而不必考虑是否使用quack()或bark(),甚至在发出正确的消息之前确定类型。

6

除了已有的帖子,有时在大型项目中编写接口可以帮助开发人员同时处理不同组件。您只需要预定义接口并编写代码,而其他开发人员则编写与您正在实现的接口相对应的代码。


5
即使我们不依赖于抽象类,编程到接口也是有优势的。编程到接口可以强制我们使用对象的上下文适当的子集,这有助于:1.防止我们做出上下文不适当的事情;2.让我们在未来安全地更改实现。例如,考虑一个Person类,它实现了Friend和Employee接口。
class Person implements AbstractEmployee, AbstractFriend {
}

在庆祝一个人的生日时,我们编程使用 Friend 接口,以避免像对待 Employee 那样对待这个人。
function party() {
    const friend: Friend = new Person("Kathryn");
    friend.HaveFun();
}

在人员的工作环境中,我们编程到 Employee 接口,以防止工作区域模糊。
function workplace() {
    const employee: Employee = new Person("Kathryn");
    employee.DoWork();
}

很好。我们在不同的情境下表现得恰当,我们的软件也运作良好。

如果在遥远的未来,我们的业务变成与狗打交道,我们可以相对容易地更改软件。首先,我们创建一个实现了FriendEmployee接口的Dog类。然后,我们将new Person()安全地更改为new Dog()。即使这两个函数有数千行代码,这个简单的编辑也能够工作,因为我们知道以下内容是正确的:

  1. 函数party仅使用PersonFriend子集。
  2. 函数workplace仅使用PersonEmployee子集。
  3. Dog类实现了FriendEmployee接口。

另一方面,如果partyworkplace中的任何一个都是针对Person编程,那么就存在着对Person特定代码的风险。从Person更改为Dog将要求我们仔细检查代码以删除Dog不支持的任何Person特定代码。

寓意:面向接口编程有助于我们的代码表现得恰当并为改变做好准备。它还使我们的代码准备依赖于抽象,带来更多的优势。


1
假设你没有过于宽泛的接口。 - Casey

5
如果我正在编写一个新的类Swimmer来添加功能swim(),并且需要使用一个名为Dog的类的对象,并且这个Dog类实现了声明swim()的接口Animal。 在层次结构的顶部(Animal),它非常抽象,而在底部(Dog)则非常具体。“面向接口编程”的方式是,当我编写Swimmer类时,我希望针对尽可能高的那个接口来编写代码,这种情况下是一个动物对象。 接口没有实现细节,因此使您的代码松散耦合。实现细节可以随时间而改变,但是它不会影响剩余的代码,因为您与接口进行交互而不是实现。 您不关心实现的样子...您只知道将有一个类来实现接口。

4

它也非常适合单元测试,您可以将符合接口要求的自定义类注入到依赖于它的类中。


4
以前的回答都侧重于基于抽象编程以实现可扩展性和松耦合,虽然这些非常重要,但可读性同样重要。可读性使得其他人(包括未来的自己)能够以最小的努力理解代码。这就是为什么可读性利用抽象。

抽象本质上比实现更简单。抽象省略了细节,仅传达物体的本质或目的,而不多余的东西。 因为抽象更简单,所以我可以一次性将更多的抽象品放入脑海中,相比之下,实现则不行。

作为一个程序员(无论使用任何语言),我始终心中有一个List的概念。特别是,List 允许随机访问、重复元素并保持顺序。 当我看到这样的声明:List myList = new ArrayList()时,我会感叹说,棒极了,这是一个用我理解的(基本)方法使用的List; 我不必再思考它了。

另一方面,我不会记忆ArrayList的具体实现细节。 当我看到这个时,ArrayList myList = new ArrayList()。 我会想,糟糕, 这个 ArrayList 必须用在没有包含在List接口之内的方式中。现在我必须跟踪这个ArrayList的所有用法才能理解为什么,因为否则我将无法完全理解此代码。 当我发现100%的ArrayList用法符合List接口时,情况变得更加混乱。然后我会想...是否有一些依赖于ArrayList实现细节的代码被删除了?初始化它的程序员只是无能吗?这个应用程序是否在运行时锁定到特定的实现方式?一种我不理解的方式?

我现在对该应用程序感到困惑和不确定,而我们所讨论的只是一个简单的List。如果这是忽略其接口的复杂业务对象呢?那么我的业务领域知识就不足以理解代码的目的。
因此,即使我严格需要一个List,只在一个private方法中使用(如果更改不会破坏其他应用程序,并且可以轻松找到/替换我的IDE中的每个用法),基于抽象编程仍有利于可读性。因为抽象比实现细节更简单。可以说,基于抽象编程是遵循KISS原则的一种方式。

非常好的解释。这个参数真的很有价值。 - Lars Hansen

4
简短故事:一位邮递员被要求去家里收取包含(信件、文件、支票、礼品卡、申请书、情书)地址写在上面的封套并投递。
假设没有封套,要求邮递员挨家挨户地收取所有物品并交给其他人,邮递员可能会感到困惑。
因此最好用封套(在我们的故事中是接口)将其包装起来,然后他就能很好地完成工作了。
现在邮递员的工作仅限于收取和投递封套(他不会关心封套内部的内容)。
创建一种类型的接口,不是实际类型,但使用实际类型进行实现。
创建接口意味着您的组件可以轻松地适应其他代码。
我给你举个例子。
您有以下AirPlane接口。
interface Airplane{
    parkPlane();
    servicePlane();
}

假设您的控制器类中有关于飞机的方法,例如:
parkPlane(Airplane plane)

并且

servicePlane(Airplane plane)

您可以在程序中实现它。这不会破坏您的代码。

我的意思是,只要接受AirPlane作为参数,就不需要进行更改。

因为它将接受任何类型的飞机,如flyerhighflyrfighter等。

此外,在集合中:

List<Airplane> plane; // 将接受您所有的飞机。

下面的示例将清楚地说明这一点。


您有一架战斗机实现了它,所以

public class Fighter implements Airplane {

    public void  parkPlane(){
        // Specific implementations for fighter plane to park
    }
    public void  servicePlane(){
        // Specific implementatoins for fighter plane to service.
    }
}

同样适用于HighFlyer和其他类:
public class HighFlyer implements Airplane {

    public void  parkPlane(){
        // Specific implementations for HighFlyer plane to park
    }

    public void  servicePlane(){
        // specific implementatoins for HighFlyer plane to service.
    }
}

现在考虑使用 AirPlane 多次的控制器类,

假设你的控制器类名为 ControlPlane,如下所示,

public Class ControlPlane{ 
 AirPlane plane;
 // so much method with AirPlane reference are used here...
}

在这里,神奇的地方出现了。您可以根据需要创建任意数量的新AirPlane类型实例,而不必更改ControlPlane类的代码。

您可以添加一个实例...

JumboJetPlane // implementing AirPlane interface.
AirBus        // implementing AirPlane interface.

您可以删除之前创建的类型实例。

3
假设你有一个名为“Zebra”的产品,它可以通过插件进行扩展。它会在某个目录中搜索DLL,并加载所有这些DLL,然后使用反射找到实现“IZebraPlugin”接口的任何类,并调用该接口的方法与插件进行通信。
这使其完全独立于任何特定的插件类 - 它不关心这些类是什么。它只关心它们是否符合接口规范。
接口是定义这种可扩展性点的一种方式。与接口交互的代码更松散耦合 - 实际上它根本没有与任何其他特定代码耦合。它可以与多年后由从未见过原始开发人员的人编写的插件进行交互操作。
您也可以使用具有虚函数的基类 - 所有插件都将派生自该基类。但是这更加限制,因为一个类只能有一个基类,而它可以实现任意数量的接口。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接