为什么要使用接口?

85

我知道它们强制你去实现方法等,但我不明白的是为什么你会想要使用它们。有没有人可以给我一个好的例子或解释,告诉我为什么我要实现这个。

我理解他们强制你去实现方法等,但我无法理解为什么你想要使用它们。能否有人给出一个好的例子或者解释,告诉我为什么我要实现这些内容。

4
人们,他没有要求定义。 - Mark Brady
2
我曾经长时间苦恼于接口和继承之间的选择。例如,继承抽象类和实现接口有什么区别? 在我阅读《Head First 设计模式》之前,没有一位老师能够让我明白。这本书简洁轻松,价格也不贵。 - Boris Callens
3
从技术上讲,一个接口是仅包含抽象方法的类。因此,Java实际上具有多重继承(只允许用于接口)。这是因为多重继承的问题主要涉及实例变量。(以及部分方法实现) - Hugo
这里有几个类似问题的更多答案:接口 vs 基类 - razlebe
https://softwareengineering.stackexchange.com/a/145481 - Lakshman Rao
19个回答

77

一个具体的例子: 接口是一种很好的方式,用于指定其他人的代码必须满足的合同。

如果我在编写一个代码库,我可能会编写针对具有某些行为的对象有效的代码。最好的解决方案是在接口中指定这些行为(没有实现,只是描述),然后在我的库代码中使用实现该接口的对象的引用。

然后,任何随机的人都可以创建一个实现该接口的类,实例化该类的对象并将其传递给我的库代码并期望它能够工作。注意:当然可以严格实现一个接口,同时忽略接口的意图,因此仅实现一个接口并不能保证事情能够正常工作。愚蠢总能找到办法! :-)

另一个具体的例子: 两个团队分别开发不同组件并需要相互协作。如果两个团队在第一天坐下来并就一组接口达成一致,那么他们可以各自实现基于这些接口的组件。A团队可以构建模拟B团队组件的测试工具,并反之亦然。这样进行并行开发,减少错误。

关键点在于接口提供了一层抽象,以便你编写的代码忽略不必要的细节。

大多数教科书中使用的经典例子是排序程序。只要你有办法比较任意两个对象,就可以对任何类的对象进行排序。因此,通过实现IComparable接口,您可以使任何类都可排序,该接口强制您实现比较两个实例的方法。所有排序例程都编写为处理对IComparable对象的引用,因此一旦您实现了IComparable,就可以在您的类对象集合上使用任何这些排序例程。


接口是“最好”的解决方案吗?那继承呢?使用接口时,您必须在使用它的每个类中实现每个成员,即使这些实现相同。使用继承时,基类可以实现公共成员,而派生类仅包含自定义成员。 - DOK
3
继承往往是脆弱的。如果你在扩展超类时违反了隐含的“is-a”关系,那么日后你就会遇到麻烦。 - Robert Van Hoose
Revah:好的,你需要正确使用继承。但是使用接口会比在基类中有一个单一版本的方法(如果适用)更多地编写(和维护)重复代码。 - DOK
2
@DOK:你假设在抽象基类中编写实现是可能的。对于我提供的示例,这甚至不可能 - 必须使用接口。 - Stewart Johnson
1
如果实现接口的每个类在其实现中具有相同的代码,那么您可能希望改用继承。接口的重点是实现通常不会相同,但合同应该是相同的。 - hmcclungiii
@hmcclungiii,如果这些类使用泛型具有相同的实现,那么也可以帮助解决问题。 - jfs

14

理解接口最简单的方式是,它们允许不同的对象公开共同的功能。这使得程序员可以编写更简单、更短的代码来适配接口,只要对象实现了该接口,程序就能够正常工作。

例子1: 有许多不同的数据库提供商,例如MySQL、MSSQL和Oracle等。然而,所有的数据库对象都可以执行相同的操作,因此你会发现许多针对数据库对象的接口。如果一个对象实现了IDBConnection,则它将暴露Open()和Close()方法。所以,如果我想让我的程序在不同的数据库提供商中运行,我就需要适配接口而不是特定的提供商。

IDbConnection connection = GetDatabaseConnectionFromConfig()
connection.Open()
// do stuff
connection.Close()
通过编程使用接口(IDbconnection),我现在可以在配置中更换任何数据提供程序,而我的代码保持完全相同。这种灵活性非常有用且易于维护。但缺点是我只能执行“通用”数据库操作,可能无法充分利用每个特定提供程序所提供的优势。因此,在编程中,您必须进行权衡并确定哪种情况对您最有利。
例如,几乎所有集合都实现了名为IEnumerable的接口。IEnumerable返回一个IEnumerator,其中包含MoveNext(),Current和Reset()方法。这使C#轻松地遍历集合。它之所以能够做到这一点,是因为它公开了IEnumerable接口,因此它知道对象公开了它需要遍历它的方法。这样做两件事:1)foreach循环现在将知道如何枚举集合;2)你现在可以将强大的LINQ表达式应用到你的集合上。再次说明接口之所以如此有用,是因为所有集合都有一些共同点,它们可以被遍历。每个集合可能以不同的方式移动(链表与数组),但这就是接口的美妙之处,其实现对接口的使用者来说是隐藏和不相关的。MoveNext()会给你集合中的下一项,不管它是如何实现的。很不错,对吧?
当你设计自己的接口时,你只需要问自己一个问题:这些东西有什么共同点?一旦找到对象分享的所有内容,就将那些属性/方法抽象成一个接口,以便每个对象都可以继承它。然后,您可以使用一个接口编写针对多个对象的程序。
当然,我还要给出我最喜欢的C++多态示例,也就是动物的例子。所有动物都共享某些特征。假设它们可以移动、说话,并且它们都有一个名字。因为我刚刚确认了我的所有动物都有共同点,并且我可以将这些特性抽象成IAnimal接口。然后我创建了一个熊对象,一个猫头鹰对象和一个蛇对象,它们都实现了这个接口。之所以可以将实现相同接口的不同对象存储在一起,是因为接口表示IS-A关系。熊是动物,猫头鹰是动物,因此我可以将它们都收集起来作为动物。
var animals = new IAnimal[] = {new Bear(), new Owl(), new Snake()} // here I can collect different objects in a single collection because they inherit from the same interface

foreach (IAnimal animal in animals) 
{
    Console.WriteLine(animal.Name)
    animal.Speak() // a bear growls, a owl hoots, and a snake hisses
    animal.Move() // bear runs, owl flys, snake slithers
}
你可以看到,即使这些动物以不同的方式执行每个动作,我仍然可以针对它们所有人统一的模型进行编程,这是接口的许多好处之一。因此,接口最重要的事情就是对象有哪些共同点,以便您可以以相同的方式针对不同的对象进行编程。这样可以节省时间,创建更加灵活的应用程序,隐藏复杂性/实现细节,模拟真实世界的对象/情况,同时还有许多其他的好处。希望这可以帮助你。

10

接口定义契约,这是关键词。

当您需要在程序中定义合同但实际上不太在意满足该合同的类的其余属性时,可以使用接口。

因此,让我们看一个例子。假设您有一个提供对列表进行排序功能的方法。首先..什么是列表?您真的在乎它保存哪些元素以便排序吗?您的答案应该是否定的...在.NET(例如)中,您有一个名为IList的接口,该接口定义了列表必须支持的操作,因此您不关心表面下的实际细节。

回到示例,您实际上不知道列表中对象的类...也不必关心。如果您只能比较对象,那么您也可以对它们进行排序。因此,您声明了一个契约:

interface IComparable
{
  // Return -1 if this is less than CompareWith
  // Return 0 if object are equal
  // Return 1 if CompareWith is less than this
  int Compare(object CompareWith);
}

该合同指定必须实现一个接受对象并返回int的方法才能进行比较。现在您已经定义了一个合同,而不关心对象本身,所以可以这样做:

IComparable comp1 = list.GetItem(i) as IComparable;

if (comp1.Compare(list.GetItem(i+1)) < 0)
  swapItem(list,i, i+1)

PS:我知道这些例子有点幼稚,但它们只是示例...


一个细节是,IComparable 的契约并没有规定你需要返回 10-1,而是要求你返回 >00,和 <0。这样可以进行一些优化,比如只需通过 return this.intNum - CompareWith.intNum 来对 int 进行排序。你不关心它相对于 0 的差距有多少,只关心它是更大还是更小。 - Scott Chamberlain

10
一个典型的例子是插件架构。 A开发人员编写主应用程序,并希望确保由B,C和D开发人员编写的所有插件符合他的应用程序对它们的期望。

5
When you need different classes to share same methods you use Interfaces.

我也可以使用继承。为什么我们要使用接口?它是否能使我的代码更简单,节省一些内存,提高可读性,易于管理,灵活等等?请在你的回答中包含这些内容。 - HIRA THAKUR

4

接口在期望充分利用多态的面向对象系统中是必不可少的。

一个经典的例子可能是IVehicle(交通工具接口),它有一个Move()方法。你可以有Car、Bike和Tank类,它们实现了IVehicle。它们都可以Move(),而且你可以编写代码,不关心处理哪种类型的车辆,只要它能Move()就行。

void MoveAVehicle(IVehicle vehicle)
{
    vehicle.Move();
}

这实际上即使没有接口也可以工作。(鸭子类型。)接口只是让您告诉编译器您的意图的一种方式。 - Hugo
2
实际上,车辆不一定要是接口,它可以是一个基类。接口更适用于服务或类似的情况,您只关心合同而不是继承,因此我认为多态性实际上并不是接口的重要之处。 - Hugo
是的,你说得对。但通常有一个接口定义了超类的方法。在某种意义上,接口是一个超类,只是没有实现。 - Eric Z Beard
为什么车辆应该是一个基类?车辆唯一的共性就是它们可以移动,除此之外没有其他实现上的共性。在这种情况下使用基类并没有任何优势,反而会带来许多不利因素。 - Wedge
@Wedge - 基类能够为你提供一个默认实现,方便以后的开发。但是如果需要的话,你也可以随时在后面实现这个基类。 - Cory House
可以只用抽象类或简单的继承来完成。 - David Constantine

4

想象一个基本的接口,它定义了一个基本的CRUD机制:

interface Storable {
    function create($data);
    function read($id);
    function update($data, $id);
    function delete($id);
}

通过这个接口,你可以看出任何实现它的对象,必须具有创建、读取、更新和删除数据的功能。这可能是一个数据库连接、CSV文件阅读器、XML文件阅读器或任何其他想要使用CRUD操作的机制。
因此,你现在可以拥有类似以下的东西:
class Logger {
    Storable storage;

    function Logger(Storable storage) {
        this.storage = storage;
    }

    function writeLogEntry() {
        this.storage.create("I am a log entry");
    }
}

这个记录器不在乎你传递的是数据库连接还是操作磁盘上文件的东西。它只需要知道可以在其上调用create(),那么它就会按预期工作。

接下来出现的问题是,如果数据库和CSV文件等都可以存储数据,那么它们是否应该从通用的可存储对象中继承,从而摆脱接口的需求呢?答案是否定的...并非每个数据库连接都能实现CRUD操作,每个文件读取器也是如此。

接口定义了对象能做什么以及您需要如何使用它...而不是定义它是什么!


2
接口定义了对象能够做什么以及你需要如何使用它,而不是它是什么!这比页面上的其他所有内容都更为清晰明了。 - Mr Wednesday

4
接口是多态形式的一种。一个例子:
假设你想编写一些日志记录代码。日志将会被输出到某个地方(可能是文件,或者运行主要代码的设备上的串行端口,或者套接字,或者像/dev/null这样的丢弃位置)。你不知道它将输出到哪里:你的日志记录代码的用户需要自由决定。事实上,你的日志记录代码并不关心。它只想要能够写入字节的东西。
所以,你发明了一个名为“可以写入字节的东西”的接口。给日志记录代码提供一个该接口的实例(也许在运行时,也许在编译时配置。它仍然是多态的,只是不同类型的)。编写一个或多个实现接口的类,只需通过更改日志记录代码将使用哪个类来轻松更改日志记录的位置。其他人可以通过编写他们自己的接口实现来更改日志记录的位置,而不必更改你的代码。这基本上就是多态的意义——只需了解一个对象的足够信息以特定方式使用它,同时允许它在您不需要了解的所有方面变化。接口描述了您需要了解的内容。
C的文件描述符基本上相当于一个接口“我可以从中读取和/或写入字节”,几乎每种类型的语言在其标准库中都有此类接口:流或其他。未打字的语言通常具有表示流的非正式类型(可能称为合同)。因此,实际上您几乎永远不必真正发明这个特定的接口:您使用语言提供的内容即可。
日志和流只是一个例子——无论何时,当您可以用抽象术语描述对象应该做什么,但不想将其绑定到特定的实现/类/任何其他内容时,都会发生接口。

4
汽车上的踏板实现了一个接口。我来自美国,在那里我们行驶在道路右侧。我们的方向盘在汽车左侧。手动变速器的踏板从左到右分别是离合器->刹车->油门。当我去爱尔兰时,驾驶方式被颠倒了。汽车的方向盘在右侧,他们在道路的左侧行驶...但是踏板,啊踏板...它们实现了相同的接口...所有三个踏板都按照相同的顺序排列...所以即使类不同,操作的网络也不同,我仍然可以轻松地掌握踏板接口。我的大脑能够像其他任何汽车一样呼叫我的肌肉。
想想我们无法生活没有的众多非编程接口。然后回答你自己的问题。

1
那么,美国汽车和英国汽车类实现了汽车接口,该接口具有离合器、刹车和油门方法以及方向盘方向属性?然后,离合器、刹车和油门方法基本相同,但它们的代码必须在每个实现汽车的类中重复吗? - DOK
我并不打算将这个比喻一直延伸到编程领域。楼主说他知道它们是什么,只是不知道为什么要使用它们。我想展示一个接口在一个常见的现实物品中可能会是什么样子。有时候需要走出我们当前的思维世界才能理解一个概念。 - Mark Brady
@Mark Brady:我喜欢那个比喻。很抱歉,现在已经过去了15年。 - brohjoe
@Mark Brady:我喜欢那个比喻。很抱歉,现在已经过去了15年。 - undefined

3

有很多理由这么做。当你使用一个接口时,如果未来需要重构或重写代码,你就已经准备好了。同时,你还可以为简单操作提供一种标准化的API。

例如,如果你想编写一个类似快排的排序算法,那么你只需要能够成功比较两个对象即可对任何对象列表进行排序。如果你创建一个名为ISortable的接口,那么任何创建对象的人都可以实现ISortable接口并使用你的排序代码。

如果你正在编写使用数据库存储的代码,并且你写入了一个存储接口,你可以在以后替换掉这段代码。

接口鼓励代码的松耦合,使你拥有更大的灵活性。


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