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

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个回答

1821

这里有一些很棒的回答,涉及到接口、松耦合代码、控制反转等方面的细节。但是,有些讨论比较深奥,我想利用这个机会为大家简单解释一下为什么接口很有用。

当我第一次接触接口时,我也很困惑它们的相关性。我不明白为什么需要它们。如果我们使用像Java或C#这样的语言,我们已经有了继承,我认为接口只是继承的一种弱化表现形式。那么,为什么要费劲去实现它呢?从某种程度上说,我的观点没错,你可以把接口看作是继承的一种弱化表现形式,但除此之外,我终于明白了它们作为一种语言构造的用处,即通过将许多非相关类对象所共有的特点或行为分类来使用它们。

例如——假设你有一个SIM游戏,有以下几个类:

class HouseFly inherits Insect {
    void FlyAroundYourHead(){}
    void LandOnThings(){}
}

class Telemarketer inherits Person {
    void CallDuringDinner(){}
    void ContinueTalkingWhenYouSayNo(){}
}

显然,这两个对象在直接继承方面没有任何共同之处。但是,你可以说它们都很烦人。

假设我们的游戏需要有某种随机“东西”,当玩家吃饭时它会让他们感到烦恼。这可能是一只HouseFly或一个Telemarketer,或者两者都有,但是如何使用单个函数允许两者存在呢?并且如何要求每个不同类型的对象以相同的方式“做出它们的烦人之事”呢?

关键在于意识到,TelemarketerHouseFly虽然在建模上没有任何相似之处,但它们共享一种常见的松散解释行为。因此,让我们创建一个接口,让两者都可以实现:

interface IPest {
    void BeAnnoying();
}

class HouseFly inherits Insect implements IPest {
    void FlyAroundYourHead(){}
    void LandOnThings(){}

    void BeAnnoying() {
        FlyAroundYourHead();
        LandOnThings();
    }
}

class Telemarketer inherits Person implements IPest {
    void CallDuringDinner(){}
    void ContinueTalkingWhenYouSayNo(){}

    void BeAnnoying() {
        CallDuringDinner();
        ContinueTalkingWhenYouSayNo();
    }
}

现在我们有两个类,它们各自都可以以自己的方式变得令人讨厌。它们不需要从同一基类派生并共享共同本质特征-它们只需要满足IPest的合约-这个合约很简单。你只需要 BeAnnoying。在这方面,我们可以建模如下:

class DiningRoom {

    DiningRoom(Person[] diningPeople, IPest[] pests) { ... }

    void ServeDinner() {
        when diningPeople are eating,

        foreach pest in pests
        pest.BeAnnoying();
    }
}

这里有一个餐厅,可以容纳很多食客和害虫。请注意接口的使用。这意味着在我们的小世界中,pests 数组的成员实际上可以是 Telemarketer 对象或 HouseFly 对象。

ServeDinner 方法在用餐时被调用,我们在餐厅的人应该吃东西。在我们的小游戏中,这时我们的害虫会开始工作——每只害虫都会通过 IPest 接口被指示如何令人讨厌。通过这种方式,我们可以轻松地让 TelemarketersHouseFlys 以各自不同的方式让人讨厌,而我们只关心在 DiningRoom 对象中有一个害虫,真正的对象类型并不重要,它们彼此之间可能没有任何共同点。

这个非常牵强附会的伪代码示例(比我预期的要冗长得多)只是为了说明我们何时可以使用接口。我提前为这个示例的愚蠢表示歉意,但是希望它能有助于您的理解。当然,您在这里收到的其他回答已经涵盖了接口在设计模式和开发方法中使用的方方面面。


4
另一个需要考虑的问题是,在某些情况下,拥有一个界面来处理“可能”令人讨厌的事情可能是有用的,并且有各种对象实现BeAnnoying作为无操作;如果两个接口都存在,则此接口可以替代或添加到已经存在的“令人讨厌”的接口中(如果两个接口都存在,则“令人讨厌的事物”接口应继承“可能令人讨厌的事物”接口)。使用这样的接口的缺点是实现可能会负担起实现大量存根方法的工作。优点是... - supercat
4
这些方法并不意味着代表抽象方法——它们的实现与问题的重点在接口上无关。 - Peter Meyer
47
如果有人想要了解更多相关内容,将行为封装成像IPest一样的方式,被称为策略模式。 - nckbrz
10
有趣的是,你没有指出 IPest[] 中的对象是 IPest 引用,因此可以调用它们具有的 BeAnnoying() 方法,而不能在没有转换的情况下调用其他方法。但是,每个对象的独立 BeAnnoying() 方法将被调用。 - D. Ben Knoble
4
非常好的解释...我只是想在这里说一句:我从未听说过接口是某种松散的继承机制,而是知道继承被用作定义接口的一种不良机制(例如,在普通的Python中,你经常这样做)。 - Carlos H Romano
显示剩余16条评论

332

我向学生们举的一个具体例子是,他们应该写:

List myList = new ArrayList(); // programming to the List interface

代替

ArrayList myList = new ArrayList(); // this is bad

这两种声明在一个简短的程序中看起来完全相同,但如果你在程序中使用myList100次,你会开始看到不同。第一种声明确保您只调用由List接口定义的myList方法(因此没有ArrayList特定的方法)。如果您以这种方式编程接口,那么稍后您可以决定您真正需要什么。

List myList = new TreeList();

你只需要在那一个地方改变你的代码。 你已经知道你的其余代码不会被更改实现所破坏,因为你编程到了接口。

当谈到方法参数和返回值时,好处甚至更加明显(我认为)。以这个为例:

public ArrayList doSomething(HashMap map);

该方法声明将您绑定到两个具体实现(ArrayListHashMap)。一旦从其他代码调用该方法,对这些类型所做的任何更改可能意味着您还需要更改调用代码。最好编写符合接口的程序。

public List doSomething(Map map);

现在无论你返回什么样的List,或者传入什么样的Map作为参数,在doSomething方法内做出的更改都不会迫使你改变调用代码。


评论不适合进行长时间的讨论;此对话已被移至聊天室 - user3956566
我有一个问题,关于你提到的“第一次声明确保您只调用由List接口定义的myList方法(因此没有ArrayList特定的方法)。如果您以这种方式编程到接口,则稍后可以决定您真正需要List myList = new TreeList(); 并且您只需要在那个位置更改代码。”也许我误解了,我想知道为什么您需要将ArrayList更改为TreeList,如果您想“确保只调用myList上的方法”? - user3014901
3
@user3014901 你可能有很多原因想要更改你正在使用的列表类型。例如,某些类型的列表可能具有更好的查找性能。关键是,如果你编程时使用List接口,那么以后更换不同实现的代码会更容易。 - Bill the Lizard

93

编程到接口的意思是,"我需要这个功能,但我不关心它来自哪里。"

考虑一下(在Java中),List接口与ArrayListLinkedList具体类。如果我只关心我有一个包含多个数据项的数据结构,我应该通过迭代访问它,那么我会选择List(99%的情况下都是如此)。如果我知道我需要常数时间插入/删除列表的任一端,我可能会选择LinkedList具体实现(或更可能使用Queue接口)。如果我知道我需要通过索引进行随机访问,则会选择ArrayList具体类。


1
完全同意,即所做的事情与如何完成之间的独立性。通过将系统分成独立的组件,您最终得到一个简单且可重用的系统(请参见Clojure创建者的Simple Made Easy)。 - beluchin

48

编程到接口与 Java 或 .NET 中所见的抽象接口没有任何关系。它甚至不是一个面向对象编程(OOP)的概念。

它的意思是不要在对象或数据结构的内部进行操作。使用抽象程序接口(API)与您的数据进行交互。在 Java 或 C# 中,这意味着使用公共属性和方法而不是直接访问字段。对于 C,这意味着使用函数而不是原始指针。

编辑:在数据库中,这意味着使用视图和存储过程而不是直接访问表。


5
最佳答案。Gamma在这里给出了类似的解释:http://www.artima.com/lejava/articles/designprinciples.html(见第二页)。他提到了面向对象的概念,但你是正确的:它比面向对象更广泛。 - Sylvain Rodrigue

40
使用接口是使代码易于测试并消除类之间不必要耦合的关键因素。通过创建一个定义类操作的接口,您可以让想要使用该功能的类能够在不直接依赖于您的实现类的情况下使用它。如果以后您决定更改并使用不同的实现,则只需更改实例化实现的代码部分即可。其余代码不需要更改,因为它依赖于接口而不是实现类。
这在创建单元测试时非常有用。在测试的类中,您让其依赖于接口,并通过构造函数或属性设置器将接口的实例注入到类中(或者通过允许其根据需要构建接口实例的工厂)。类在其方法中使用提供的(或创建的)接口。当您编写测试时,可以模拟或伪造接口,并提供响应于单元测试配置数据的接口。您可以这样做,因为您的被测试类仅处理接口,而不是具体实现。任何实现接口的类,包括Mock或fake类,都可以胜任。
编辑:下面是Erich Gamma讨论他的名言“按照接口而非实现编程”的文章链接。 http://www.artima.com/lejava/articles/designprinciples.html

6
请再次仔细阅读此次采访:当时Gamma提到的是面向对象概念中的接口,而不是JAVA或C#中特定的类(ISomething)。问题在于,大多数人都认为他在谈论关键字,因此现在我们有了很多不必要的接口(ISomething)。 - Sylvain Rodrigue
1
非常好的面试。请注意未来的读者,面试有四页。我差点在看到它之前关闭了浏览器。 - Ad Infinitum

38

你应该了解反转控制(Inversion of Control):

在这种情况下,你不会写这样的代码:

IInterface classRef = new ObjectWhatever();

你可以写成这样:

IInterface classRef = container.Resolve<IInterface>();

这可以用于规则-based的设置在 container 对象中,并为您构建实际的对象,可以是ObjectWhatever。重要的是,您可以使用另一种类型的对象替换此规则,并且您的代码仍将正常工作。

如果我们不考虑IoC,您可以编写能够与执行特定操作的对象通信的代码,但不知道对象的类型或它如何执行操作。

这在传递参数时非常有用。

至于你括号中提出的问题“另外,你怎么编写一个接收实现接口的对象的方法?这可行吗?”,在C#中,您可以像这样简单地使用接口类型作为参数类型:

public void DoSomethingToAnObject(IInterface whatever) { ... }

这段代码与“与执行特定操作的对象交互”完美契合。上面定义的方法知道从对象中期望什么,即它实现了IInterface中的所有内容,但是它不关心对象的类型,只关心它遵循合同,这就是接口的作用。

例如,你可能熟悉计算器,并在日常生活中使用过许多种,但大多数时候它们都不同。然而,你知道标准计算器应该如何工作,因此即使不能使用每个计算器都具有但其他计算器没有的特定功能,你仍然能够使用它们。

这就是接口之美。您可以编写一段代码,知道它将得到传递给它的对象,可以期望某些行为。它不关心对象是什么类型,只关心它是否支持所需的行为。

让我举个具体的例子。

我们为Windows表单构建了一个自定义翻译系统。该系统循环遍历表单上的控件并翻译每个控件的文本。该系统知道如何处理基本控件,例如具有Text属性的控件等基本内容,但对于任何基本内容,它都不足以胜任。

现在,由于控件继承自我们无法控制的预定义类,因此我们可以采取以下三种方法之一:

  1. 构建支持,使翻译系统能够检测它正在使用哪种类型的控件,并翻译正确的内容(维护噩梦)
  2. 在基类中构建支持(不可能,因为所有控件都继承自不同的预定义类)
  3. 添加接口支持

因此我们选择了第三种方法。我们的所有控件都实现了ILocalizable接口,这是一个接口,为我们提供了一种方法,即将“自身”翻译成一组翻译文本/规则的容器。因此,表单不需要知道它找到了哪种控件,只需要知道它实现了特定接口,并且知道有一种方法可以调用以将控件本地化。


33
为什么一开始就提及控制反转(IoC),这只会增加更多的困惑。 - Kevin Le - Khnle
1
同意,我认为针对接口编程只是一种使IoC更加容易和可靠的技术。 - terjetyl

37

“按照接口而不是实现编码”与Java及其接口构造无关。

这个概念在《设计模式/四人帮》一书中得到了广泛的推崇,但它很可能在那之前就已经存在了。这个概念在Java诞生之前就已经存在

Java接口构造是为了帮助实现这种思想(以及其他事情),但人们过于关注构造本身而忽视了最初的意图。然而,这就是为什么我们在Java、C++、C#等语言中拥有公共和私有方法和属性的原因。

这意味着只与一个对象或系统的公共接口进行交互。不要担心或预先考虑它的内部实现方式。不用担心它是如何实现的。在面向对象的代码中,这就是为什么我们有公共和私有方法/属性的原因。我们打算使用公共方法,因为私有方法仅用于类内部使用。它们组成了类的实现,可以根据需要更改,而不影响公共接口。假设对于功能而言,调用具有相同参数的类上的方法每次都会执行相同的操作并产生相同的结果。它允许作者更改类的工作方式,即其实现方式,而不会影响人们如何与它交互。

您可以在从未使用过接口构造的情况下按照接口而不是实现进行编程。 您可以在C++中按照接口而不是实现进行编程,因为C++没有接口构造。只要通过公共接口(契约)进行交互而不是调用系统内部对象上的方法,就可以更加稳健地集成两个大型企业系统。如果根据接口而不是实现来实现,则期望接口始终以相同的预期方式对同样的输入参数做出反应。这个概念适用于许多地方。

请摒弃Java接口与“按照接口而非实现进行编码”概念有任何关系的想法。它们可以帮助应用这个概念,但它们不是这个概念。


2
第一句话就把问题解决了,这应该是被采纳的答案。 - madumlao

14

听起来您理解接口如何工作,但不确定何时使用它们以及它们提供的优势。以下是几个示例,说明何时使用接口是有意义的:

// if I want to add search capabilities to my application and support multiple search
// engines such as Google, Yahoo, Live, etc.

interface ISearchProvider
{
    string Search(string keywords);
}

那么我就可以创建 GoogleSearchProvider、YahooSearchProvider、LiveSearchProvider 等。

// if I want to support multiple downloads using different protocols
// HTTP, HTTPS, FTP, FTPS, etc.
interface IUrlDownload
{
    void Download(string url)
}

// how about an image loader for different kinds of images JPG, GIF, PNG, etc.
interface IImageLoader
{
    Bitmap LoadImage(string filename)
}

然后创建JpegImageLoader、GifImageLoader、PngImageLoader等。

大多数插件和插件系统都使用接口。

另一个常见用途是存储库模式。比如说我想从不同的来源加载邮政编码列表。

interface IZipCodeRepository
{
    IList<ZipCode> GetZipCodes(string state);
}

那我可以创建一个XMLZipCodeRepository、SQLZipCodeRepository、CSVZipCodeRepository等等。对于我的Web应用程序,我经常在SQL数据库准备好之前就提前创建XML repositories,以便能够快速上线应用程序。一旦数据库准备好了,我就会编写一个SQLRepository来替换XML版本。我的其余代码保持不变,因为它仅基于接口运行。

方法可以接受接口,如:

PrintZipCodes(IZipCodeRepository zipCodeRepository, string state)
{
    foreach (ZipCode zipCode in zipCodeRepository.GetZipCodes(state))
    {
        Console.WriteLine(zipCode.ToString());
    }
}

13
很多解释都已经有了,但为了更简单易懂,请看一个例子: List。可以用以下方式实现列表:
  1. 一个内部数组
  2. 一个链接列表
  3. 其他实现
通过构建到接口,例如 List 接口,你只需要按照 List 的定义或其在现实中的含义来编写代码。
你可以在内部使用任何类型的实现,例如 array 实现。但是,如果你因为某种原因(比如 Bug 或性能)希望更改实现方式,则只需将声明从 List<String> ls = new ArrayList<String>() 更改为 List<String> ls = new LinkedList<String>()
在代码的任何其他地方,你都不需要更改任何内容;因为其他所有内容都是基于 List 的定义构建的。

12

当你拥有一组相似的类时,将代码改造成更加可扩展和易于维护。我是一名初级程序员,没有专业技术,但我刚完成了一个需要类似操作的项目。

我在客户端软件上工作,与运行医疗设备的服务器通信。我们正在开发这个设备的新版本,其中包含一些客户必须进行配置的新组件。有两种类型的新组件,它们是不同的,但也非常相似。基本上,我需要创建两个配置表单,两个列表类,两个全部内容。

我决定为每个控件类型创建一个抽象基类,该类几乎包含所有真正的逻辑,然后派生类型来处理两个组件之间的差异。但是,如果我必须一直考虑类型,那么基类将无法对这些组件执行操作(嗯,它们可以,但每个方法中会有“if”语句或开关)。

我为这些组件定义了一个简单的接口,并且所有基类都与此接口交互。现在,当我更改某些内容时,几乎在任何地方都能够“只需工作”,而且我没有重复的代码。


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