关于语言不可知论:”程序到接口”是什么意思?

What does it mean to “program to an interface”?

我已经看过几次了,我不清楚这是什么意思。你什么时候为什么要这样做?

我知道接口是做什么的,但我不清楚这一点,这让我觉得我错过了正确使用它们。

如果你这样做,是不是:

1
IInterface classRef = new ObjectWhatever()

您可以使用任何实现IInterface的类吗?你什么时候需要这样做?我唯一能想到的是,如果您有一个方法,并且您不确定除了实现IInterface之外,将传递什么对象。我想不出你需要多久做一次。

另外,您如何编写一个方法来接受实现接口的对象?有可能吗?


对于这个问题,这里有一些很好的答案,这些答案涉及到各种各样的细节,比如接口、松散耦合代码、控制反转等等。有一些相当令人兴奋的讨论,所以我想借此机会把事情分解一下,以理解为什么界面是有用的。

当我第一次接触到接口时,我也对它们的相关性感到困惑。我不明白你为什么需要它们。如果我们使用像Java或C语言这样的语言,我们已经有了继承,我把接口看作是一种较弱的继承和思考方式,"为什么要麻烦?"从某种意义上说,我是对的,你可以把接口看作是一种弱继承形式,但除此之外,我最终理解了它们作为一种语言结构的使用,把它们看作是一种对许多潜在的非相关对象类所表现出的共同特征或行为进行分类的方法。

例如,假设您有一个SIM游戏,并且有以下类:

1
2
3
4
5
6
7
8
9
class HouseFly inherits Insect {
    void FlyAroundYourHead(){}
    void LandOnThings(){}
}

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

显然,这两个对象在直接继承方面没有任何共同点。但是,你可以说他们都很讨厌。

假设我们的游戏需要一些随机的东西,当游戏玩家吃饭时,这些东西会让他们感到恼火。这可能是一个HouseFly或一个Telemarketer,或者两者兼而有之——但是如何允许两者都具有一个函数呢?你如何要求每种不同类型的物体以同样的方式"做它们令人讨厌的事情"?

要认识到的关键是,TelemarketerHouseFly都有一个共同的松散解释的行为,即使它们在建模方面没有任何相似之处。因此,让我们创建一个既可以实现以下功能的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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的契约——这个契约很简单。你只需要1〔5〕个字就行了。在这方面,我们可以建立以下模型:

1
2
3
4
5
6
7
8
9
10
11
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物体中有什么东西是害虫,我们并不真正关心它是什么,它们不能与其他物体有任何共同点。

这个非常做作的伪代码示例(拖的时间比我预期的要长得多)仅仅是为了说明在我们可能使用接口的时候,最终为我打开了灯的那种东西。我提前为这个例子的愚蠢道歉,但希望它有助于你的理解。而且,可以肯定的是,您在这里收到的其他已发布的答案实际上涵盖了目前在设计模式和开发方法中使用界面的全部内容。


我给学生的具体例子是他们应该写

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

而不是

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

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

1
List myList = new TreeList();

你只需要在那一个地方修改你的代码。您已经知道,您的代码的其余部分不会做任何由于编程到接口而更改实现而被破坏的事情。

当您谈论方法参数和返回值时,其好处更为明显(我认为)。例如:

1
public ArrayList doSomething(HashMap map);

该方法声明将您与两个具体的实现(ArrayListHashMap联系起来。一旦从其他代码调用该方法,对这些类型的任何更改都可能意味着您还必须更改调用代码。最好编程到接口。

1
public List doSomething(Map map);

现在,不管您返回的是哪种List,还是作为参数传入的是哪种Map。在doSomething方法中所做的更改不会强制您更改调用代码。


编程到一个接口就是说,"我需要这个功能,我不在乎它来自哪里。"

考虑(在Java中),EDCOX1的1接口与EDCOX1,2,EDCX1,3个具体的类。如果我所关心的是我有一个包含多个数据项的数据结构,我应该通过迭代访问这些数据项,那么我会选择一个List(这是99%的时间)。如果我知道需要从列表的任一端不断地插入/删除,我可以选择LinkedList具体实现(或者更可能使用队列接口)。如果我知道我需要按索引随机访问,我会选择ArrayList具体类。


除了消除类之间不必要的耦合之外,使用接口是使代码易于测试的一个关键因素。通过创建定义类上操作的接口,可以允许希望使用该功能的类使用该功能,而不直接依赖于实现类。如果稍后您决定更改和使用不同的实现,您只需要更改实现实例化的代码部分。其余的代码不需要更改,因为它依赖于接口,而不是实现类。

这对于创建单元测试非常有用。在被测试的类中,您让它依赖于接口,并通过构造函数或属性设置器将接口的实例注入类(或允许它根据需要构建接口实例的工厂)。类在其方法中使用提供的(或创建的)接口。当您开始编写测试时,您可以模拟或伪造接口,并提供一个接口,该接口用单元测试中配置的数据作出响应。您可以这样做,因为您的测试类只处理接口,而不是具体的实现。任何实现接口的类,包括模拟类或假类,都可以做到。

编辑:下面是一篇文章的链接,其中ErichGamma讨论了他的引言,"程序到接口,而不是实现。"

http://www.artima.com/lejava/articles/designprinciples.html


你应该研究控制反转:

  • Martin Fowler:控制容器倒置和依赖注入模式
  • 维基百科:控制反转

在这种情况下,你不会写下:

1
IInterface classRef = new ObjectWhatever();

你可以这样写:

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

这将进入container对象中基于规则的设置,并为您构造实际的对象,该对象可以是任何对象。重要的是,您可以将此规则替换为完全使用其他类型对象的规则,并且您的代码仍然可以工作。

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

这在传递参数时很有用。

至于带括号的问题"另外,如何编写一个接受实现接口的对象的方法?"有可能吗?",在C中,您只需使用接口类型作为参数类型,如下所示:

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

这直接插入到"与做特定事情的对象交谈"中。上面定义的方法知道从对象期望什么,它实现了界面中的所有内容,但它不关心它是哪种类型的对象,只关心它遵守契约,这就是接口。

例如,你可能对计算器很熟悉,可能在你的日子里用过不少,但大多数时候它们都不一样。另一方面,你知道一个标准的计算器应该如何工作,所以你可以全部使用它们,即使你不能使用每个计算器都没有的特定功能。

这就是界面的美。您可以编写一段代码,它知道它将得到传递给它的对象,它可以从中预期某些行为。它不关心它是什么样的对象,只关心它支持所需的行为。

我给你举个具体的例子。

我们有一个为Windows窗体定制的翻译系统。该系统循环访问窗体上的控件,并在每个控件中转换文本。系统知道如何处理基本控件,比如具有文本属性的控件类型,以及类似的基本内容,但是对于任何基本控件,它都是不足的。

现在,由于控件继承自我们无法控制的预定义类,因此我们可以执行以下三项操作之一:

  • 为我们的翻译系统建立支持,以明确检测它使用的控制类型,并翻译正确的位(维护噩梦)
  • 将支持构建到基类中(不可能,因为所有控件都继承自不同的预定义类)
  • 添加接口支持
  • 所以我们选了3号。我们所有的控件都实现了ILocalizable,这是一个接口,它给了我们一个方法,将"本身"转换为翻译文本/规则的容器。因此,表单不需要知道找到了哪种控件,只需要实现特定的接口,并且知道有一个方法可以调用该方法来本地化控件。


    编程到一个接口与抽象接口完全没有关系,就像我们在Java或.NET中看到的那样。这甚至不是OOP概念。

    它真正的意思是不要乱动对象或数据结构的内部。使用抽象程序接口或API与数据交互。在Java或C语言中,这意味着使用公共属性和方法而不是原始字段访问。对于C,这意味着使用函数而不是原始指针。

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


    代码到接口不是实现,与Java无关,也没有它的接口构造。

    这一概念在四本书的模式/帮派中得到了突出的体现,但最有可能出现在这之前。这个概念肯定在Java存在之前就已经存在了。

    Java接口构建是为了帮助这个想法(除其他事项外),人们已经过于关注结构作为意义的中心而不是最初的意图。然而,这是我们在爪哇、C++、C等中都有公开和私有的方法和属性的原因。

    它意味着只与对象或系统的公共接口交互。不要担心,甚至不要预测它是如何在内部完成的。不要担心它是如何实现的。在面向对象的代码中,这就是我们拥有公共方法/属性与私有方法/属性的原因。我们打算使用公共方法,因为私有方法只在类内部使用。它们构成了类的实现,可以根据需要进行更改,而无需更改公共接口。假设在功能方面,每次使用相同的参数调用类时,类上的方法都将以相同的预期结果执行相同的操作。它允许作者更改类的工作方式及其实现,而不破坏人们与类的交互方式。

    您可以编程到接口,而不是在不使用接口构造的情况下实现。您可以编程到接口,而不是C++中的实现,它没有接口构造。只要两个大型企业系统通过公共接口(契约)进行交互,而不是对系统内部的对象调用方法,就可以更加牢固地集成它们。对于相同的输入参数,接口总是以相同的预期方式进行响应;如果是对接口而不是对实现进行实现的话。这个概念在许多地方都有效。

    动摇Java接口有什么东西,而不是"程序到接口,而不是实现"的概念。它们可以帮助应用概念,但它们不是概念。


    听起来您理解接口是如何工作的,但不确定何时使用它们以及它们提供了哪些优势。下面是一些界面何时有意义的示例:

    1
    2
    3
    4
    5
    6
    7
    // 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等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 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)
    }

    然后创建jPregimageLoader、gifimageLoader、pngimageLoader等。

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

    另一个流行的用法是存储库模式。假设我想加载来自不同来源的邮政编码列表

    1
    2
    3
    4
    interface IZipCodeRepository
    {
        IList<ZipCode> GetZipCodes(string state);
    }

    然后我可以为我的Web应用程序创建一个xmlzipcoderepository、sqlzipcoderepository、csvzipcoderepository等。我经常在早期创建XML存储库,这样我就可以在SQL数据库准备就绪之前启动并运行一些东西。一旦数据库准备就绪,我就编写一个SQLRepository来替换XML版本。我的代码的其余部分保持不变,因为它只在接口上运行。

    方法可以接受接口,例如:

    1
    2
    3
    4
    5
    6
    7
    PrintZipCodes(IZipCodeRepository zipCodeRepository, string state)
    {
        foreach (ZipCode zipCode in zipCodeRepository.GetZipCodes(state))
        {
            Console.WriteLine(zipCode.ToString());
        }
    }

    当您拥有一组类似的类时,它使您的代码更具可扩展性,并且更易于维护。我是一个初级程序员,所以我不是专家,但我刚刚完成了一个需要类似东西的项目。

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

    我决定最好为每个控制类型创建一个抽象的基类,它可以容纳几乎所有的实际逻辑,然后派生类型来处理两个组件之间的差异。但是,如果我一直担心类型的话,基类就不能在这些组件上执行操作(好吧,它们本来可以,但是在每个方法中都会有一个"if"语句或开关)。

    我为这些组件定义了一个简单的接口,所有的基类都与这个接口进行了对话。现在,当我改变一些东西时,它几乎在任何地方都"正常工作",我没有代码重复。


    如果你用Java编程,JDBC就是一个很好的例子。JDBC定义了一组接口,但没有提到实现。可以针对这组接口编写应用程序。理论上,您选择一些JDBC驱动程序,您的应用程序就可以正常工作了。如果您发现有一个更快、更好或更便宜的JDBC驱动程序,或者出于任何原因,您可以在理论上重新配置您的属性文件,而不必对应用程序进行任何更改,您的应用程序仍然可以工作。


    编程到接口是很棒的,它促进了松散耦合。正如@lassevk所提到的,控制反转是一个很好的用途。

    此外,还要研究可靠的原则。这是一个视频系列

    它经过硬编码(强耦合示例),然后查看接口,最后进入IOC/DI工具(ninject)


    我是这个问题的后来居上者,但我想在这里提到"程序到接口,而不是实现"这一行在GoF(四人帮)设计模式书中有过一些很好的讨论。

    它在第18页指出:

    Program to an interface, not an implementation

    Don't declare variables to be instances of particular concrete classes. Instead, commit only to an interface defined by an abstract class. You will find this to be a common theme of the design patterns in this book.

    最重要的是,它始于:

    There are two benefits to manipulating objects solely in terms of the interface defined by abstract classes:

  • Clients remain unaware of the specific types of objects they use, as long as the objects adhere to the interface that clients expect.
  • Clients remain unaware of the classes that implement these objects. Clients only know about the abstract class(es) defining the interface.
  • 所以换句话说,不要把它写在类中,这样它就有了一个用于鸭子的quack()方法,然后是一个用于狗的bark()方法,因为它们对于类(或子类)的特定实现来说太具体了。相反,使用基本类中足够通用的名称编写方法,例如giveSound()move(),以便它们可以用于鸭子、狗甚至汽车,然后您类的客户可以直接说.giveSound(),而不是考虑是否使用quack()bark(),甚至决定是否使用quack()bark()。e在发出要发送到对象的正确消息之前键入。


    除了已经选择的答案(以及这里的各种信息性文章),我强烈建议您抓取一份head-first设计模式的副本。这是一个非常容易阅读和将直接回答您的问题,解释为什么它是重要的,并向您展示许多编程模式,您可以使用这一原则(和其他)。


    有很多解释,但要让它更简单。以List为例。一个人可以用如下方式实现一个列表:

  • 内部数组
  • 链表
  • 其他实施
  • 通过构建一个接口,比如List。您只需对列表的定义或实际中List的含义进行编码。

    您可以在内部使用任何类型的实现,例如array实现。但是假设您出于某种原因想要更改实现,比如bug或性能。然后你只需要把申报单List ls = new ArrayList()改为List ls = new LinkedList()

    在代码中没有其他地方,您将不得不更改任何其他内容;因为所有其他内容都是基于List的定义构建的。


    它也适用于单元测试,您可以将自己的类(满足接口的要求)注入到依赖它的类中。


    为了添加到现有的文章中,当开发人员同时处理不同的组件时,有时对接口进行编码有助于大型项目。您所需要的只是预先定义接口并向它们编写代码,而其他开发人员则向您正在实现的接口编写代码。


    C++解释。

    把接口想象成类公共方法。

    然后,您可以创建一个"依赖"于这些公共方法的模板,以便执行自己的函数(它在类公共接口中定义函数调用)。假设这个模板是一个容器,就像一个向量类,它所依赖的接口是一个搜索算法。

    定义函数/接口向量的任何算法类调用都将满足"契约"(如原始回复中所述)。算法甚至不需要是同一个基类;唯一的要求是向量所依赖的函数/方法(接口)是在算法中定义的。

    所有这些的要点是,只要提供向量所依赖的接口(气泡搜索、顺序搜索、快速搜索),就可以提供任何不同的搜索算法/类。

    您可能还希望设计其他容器(列表、队列),通过让它们满足您的搜索算法所依赖的接口/契约,从而利用与向量相同的搜索算法。

    这节省了时间(OOP原则"代码重用"),因为您可以一次而不是一次又一次地针对您创建的每个新对象编写一个算法,而不会过度复杂地使用过度增长的继承树问题。

    至于"错过"如何操作,大时间(至少在C++中),因为这是标准模板库的大部分框架是如何运作的。

    当然,当使用继承类和抽象类时,接口编程的方法会发生变化;但是原理是相同的,公共函数/方法是类接口。

    这是一个巨大的主题,也是设计模式的基础原则之一。


    所以,为了正确理解这个问题,接口的优点是我可以将方法的调用与任何特定的类分离开来。相反,创建一个接口实例,在该实例中,实现来自我选择的实现该接口的任何类。因此,允许我拥有许多类,它们具有相似但略有不同的功能,在某些情况下(与接口意图相关的情况),它们不关心它是哪个对象。

    例如,我可以有一个移动界面。一种方法,它使某个东西"移动",任何实现移动界面的对象(人、车、猫)都可以被传入并告诉移动。没有方法,每个人都知道它是什么类型的类。


    假设您有一个名为"Zebra"的产品,可以通过插件进行扩展。它通过在某个目录中搜索dll来查找插件。它加载所有这些DLL并使用反射来查找实现IZebraPlugin的任何类,然后调用该接口的方法与插件通信。

    这使得它完全独立于任何特定的插件类——它不关心类是什么。它只关心它们是否满足接口规范。

    接口是这样定义可扩展性点的一种方法。与接口对话的代码是松散耦合的——事实上,它根本没有耦合到任何其他特定的代码。它可以与几年后由从未见过原始开发人员的人编写的插件进行交互操作。

    您可以使用一个带有虚拟函数的基类-所有插件都将从基类派生。但这更具限制性,因为一个类只能有一个基类,而它可以实现任意数量的接口。


    在Java中,这些具体类都实现了字符序列接口:

    CharBuffer, String, StringBuffer,
    StringBuilder

    这些具体的类除了对象之外没有一个公共的父类,因此没有任何与它们相关的东西,除了它们各自与字符数组有关、表示这些字符或操纵这些字符。例如,一旦字符串对象被实例化,就不能更改字符串的字符,而可以编辑StringBuffer或StringBuilder的字符。

    然而,这些类中的每一个都能够适当地实现CharSequence接口方法:

    1
    2
    3
    4
    char charAt(int index)
    int length()
    CharSequence subSequence(int start, int end)
    String toString()

    在某些情况下,用于接受字符串的Java类库类已被修改为现在接受字符序列接口。因此,如果您有一个StringBuilder实例,而不是提取一个String对象(这意味着实例化一个新的对象实例),那么可以在实现CharSequence接口时传递StringBuilder本身。

    对于可以将字符附加到基础具体类对象实例的实例中的任何情况,某些类实现的可附加接口都有许多相同的好处。所有这些具体类都实现了可附加的接口:

    BufferedWriter, CharArrayWriter,
    CharBuffer, FileWriter, FilterWriter,
    LogStream, OutputStreamWriter,
    PipedWriter, PrintStream, PrintWriter,
    StringBuffer, StringBuilder,
    StringWriter, Writer


    短篇小说:邮递员被要求回家,并收到封面(信件、文件、支票、礼品卡、申请书、情书),上面写有递送地址。

    假设没有掩护,让邮递员回家,把所有的东西收起来,交给其他人,邮递员会感到困惑,

    所以更好用盖子把它包起来(在我们的故事中,它是接口),然后他会做好他的工作。

    现在邮递员的工作是只接收和递送封皮……(他不在乎封皮里面有什么)。

    创建不是实际类型的interface类型,而是使用实际类型实现。

    "创建到接口"意味着您的组件很容易适应代码的其余部分。

    我给你举个例子。

    你有如下的飞机接口。

    1
    2
    3
    4
    interface Airplane{
        parkPlane();
        servicePlane();
    }

    假设您的控制器类中有如下的方法:

    1
    parkPlane(Airplane plane)

    1
    servicePlane(Airplane plane)

    在程序中实现。它不会破坏您的代码。我的意思是,只要它接受像AirPlane那样的论点,就不需要改变。

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

    此外,在一个集合中:

    List plane;//会带你所有的飞机。

    下面的例子将清楚地说明您的理解。

    你有一架战斗机来执行它,所以

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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.
        }
    }

    飞人和其他权利人也一样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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,

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

    魔法来了

    您可以根据自己的需要制作新的AirPlane类型的实例,并且不会改变

    ControlPlane类代码。

    您可以添加实例..

    1
    2
    JumboJetPlane // implementing AirPlane interface.
    AirBus        // implementing AirPlane interface.

    您可以删除实例..也是以前创建的类型。


    简单来说…如果我正在编写一个新的类swimmer来添加功能swim(),并且需要使用类say dog的对象,而这个dog类实现了声明swim()的接口animal[为了更好地理解…您可以绘制一个关于我所说内容的图]。在层次结构的顶部(动物),它是非常抽象的,而在底部(狗),它是非常具体的。我认为"编程到接口"的方式是,当我编写Swimmer类时,我想在接口上编写代码,该接口在层次结构中最上层,在本例中是动物对象。接口没有实现细节,因此使代码松散耦合。实现细节可以随时间而更改,但是它不会影响其余的代码,因为您正在交互的只是接口,而不是实现。您不关心实现是什么样子的……您只知道将有一个类来实现接口。


    接口类似于契约,您希望实现类实现在契约(接口)中编写的方法。由于Java不提供多重继承,所以"编程接口"是实现多重继承的好方法。

    如果您有一个已经扩展了其他一些类B的类A,但是您希望该类A也遵循某些指导原则或实现某个合同,那么您可以通过"编程到接口"策略来实现这一点。


    即使我们不依赖抽象,编程到接口也是有利的。

    编程到接口迫使我们使用一个与上下文相关的对象子集。这是有帮助的,因为它:

  • 阻止我们做与上下文无关的事情,以及
  • 让我们在将来安全地更改实现。
  • 例如,考虑一个实现FriendEmployee接口的Person类。

    1
    2
    3
    class Person implements AbstractEmployee, AbstractFriend {

    }

    在此人生日的背景下,我们编程到Friend接口,以防止将此人视为Employee

    1
    2
    3
    4
    function party() {
        const friend: Friend = new Person("Kathryn");
        friend.HaveFun();
    }

    在工作环境中,我们编程到Employee界面,以防止模糊工作场所边界。

    1
    2
    3
    4
    function workplace() {
        const employee: Employee = new Person("Kathryn");
        employee.DoWork();
    }

    伟大的。我们在不同的环境中都表现得很好,我们的软件工作得很好。

    在遥远的将来,如果我们的业务改变为与狗共事,我们可以相当容易地改变软件。首先,我们创建了Dog类,它实现FriendEmployee。然后,我们安全地将new Person()改为new Dog()。即使这两个函数都有数千行代码,这种简单的编辑也会起作用,因为我们知道以下是正确的:

  • 函数party只使用PersonFriend子集。
  • 函数workplace只使用PersonEmployee子集。
  • Dog实现FriendEmployee接口。
  • 另一方面,如果partyworkplacePerson进行编程,则两者都有使用Person特定代码的风险。从Person改为Dog需要我们对代码进行梳理,以消除Dog不支持的任何Person特定代码。

    道德:接口编程有助于我们的代码行为得体并准备好进行更改。它还让我们的代码依赖于抽象,这带来了更多的优势。


    Q: - ..."You could use any class that implements interface?"
    A: - Yes.

    Q: -..."When would you need to do that?"
    A: - Each time you need a class(es) that implements interface(s).

    注意:我们无法实例化未由类实现的接口-true。

    • 为什么?
    • 因为接口只有方法原型,而不是定义(只是函数名,而不是逻辑)

    AnIntf anInst = new Aclass();
    // we could do this only if Aclass implements AnIntf.
    // anInst will have Aclass reference.

    注:现在我们可以理解如果bclass和cclass实现相同的dintf会发生什么。

    1
    2
    3
    4
    5
    Dintf bInst = new Bclass();  
    // now we could call all Dintf functions implemented (defined) in Bclass.

    Dintf cInst = new Cclass();  
    // now we could call all Dintf functions implemented (defined) in Cclass.

    我们所拥有的:相同的接口原型(接口中的函数名),并调用不同的实现。

    参考文献:原型-维基百科


    程序到接口允许无缝地更改由接口定义的合同的实现。它允许契约和特定实现之间的松耦合。

    IInterface classRef = new ObjectWhatever()

    You could use any class that implements IInterface? When would you need to do that?

    举个好例子,看看这个SE问题。

    为什么Java类的接口应该是首选的?

    does using an Interface hit performance?

    if so how much?

    对。它将在几秒钟内有轻微的性能开销。但是,如果您的应用程序需要动态地更改接口的实现,那么不要担心性能影响。

    how can you avoid it without having to maintain two bits of code?

    如果应用程序需要接口的多个实现,不要试图避免它们。如果接口与一个特定的实现没有紧密耦合,您可能需要部署补丁来将一个实现更改为另一个实现。

    一个好的用例:战略模式的实现:

    战略模式的现实例子


    "程序到接口"意味着不要以正确的方式提供硬代码,这意味着应该在不破坏先前功能的情况下扩展代码。只是扩展,而不是编辑以前的代码。


    我不认为interface是语言中最重要的东西:它更常用于类继承。但不管怎样,它们很重要!例如(这是Java代码,但它可以简单地适应C#或许多其他语言):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    interface Convertable<T> {

        T convert();
    }

    public class NumerableText implements Convertable<Integer> {

        private String text ="";

        public NumerableText() { }

        public NumerableText(String text) {
            this.text = text;
        }

        public String getText() {
            return this.text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public Integer convert() {
            return this.text.hashCode();
        }
    }

    public class NumerableTextArray implements Convertable<Integer> {

        private String[] textArray ="";

        public NumerableTextArray() { }

        public NumerableTextArray(String[] textArray) {
            this.textArray = textArray;
        }

        public String[] getTextArray() {
            return this.textArray;
        }

        public void setTextArray(String[] text) {
            this.textArray = textArray;
        }

        public Integer convert() {
            Integer value = 0;
            for (String text : textArray)
                value += text.hashCode();
            return value;
        }
    }

    public class Foo {

        public static void main() {
            Convertable<Integer> num1 = new NumerableText("hello");
            Convertable<Integer> num2 = new NumerableTextArray(new String[] {"test n°1","test n°2" });
            System.out.println(String.valueOf(num1.convert()));
            System.out.println(String.valueOf(num2.convert()));
            //Here are you two numbers generated from two classes of different type, but both with the method convert(), which allows you to get that number.
        }
    }

    我们先从一些定义开始:

    接口n.由对象操作定义的所有签名集称为对象的接口。

    类型n.特定接口

    上面定义的接口的一个简单示例是所有PDO对象方法,如query()commit()close()等,作为一个整体,而不是单独的。这些方法,即它的接口定义了可以发送到对象的完整消息集和请求。

    上面定义的类型是一个特定的接口。我将使用组成的形状界面来演示:draw()getArea()getPerimeter()等。

    如果对象是数据库类型,我们的意思是它接受数据库接口、query()commit()等的消息/请求。对象可以有多种类型。只要数据库对象实现了它的接口,就可以让它成为shape类型,在这种情况下,这将是子类型。

    许多对象可以具有许多不同的接口/类型,并以不同的方式实现该接口。这允许我们替换对象,让我们选择要使用的对象。也称为多态性。

    客户机将只知道接口,而不知道实现。

    因此,在本质上,对接口进行编程需要生成某种抽象类,如Shape,只指定接口,如draw()getCoordinates()getArea()等。然后用不同的具体类实现圆类、方类、三角类等接口。因此,程序到接口而不是实现。


    另外,我在这里看到了很多很好的解释性答案,所以我想在这里给出我的观点,包括一些我在使用这个方法时注意到的额外信息。

    单元测试

    在过去的两年里,我写了一个爱好项目,没有为它编写单元测试。在写了大约50k行之后,我发现编写单元测试是非常必要的。我没有使用接口(或者非常谨慎地使用)。当我进行第一次单元测试时,我发现这很复杂。为什么?

    因为我必须创建很多类实例,用于作为类变量和/或参数输入。因此,测试看起来更像集成测试(因为所有的类都绑定在一起,所以必须建立一个完整的类"框架")。

    害怕接触所以我决定使用接口。我担心我必须在任何地方(在所有使用过的类中)多次实现所有功能。在某种程度上,这是正确的,但是,通过使用继承,可以减少很多。

    接口和继承的组合我发现这个组合很好用。我举了一个很简单的例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public interface IPricable
    {
        int Price { get; }
    }

    public interface ICar : IPricable

    public abstract class Article
    {
        public int Price { get { return ... } }
    }

    public class Car : Article, ICar
    {
        // Price does not need to be defined here
    }

    通过这种方式复制代码是不必要的,但是仍然有使用汽车作为接口(ICAR)的好处。


    我坚信,这个难题应该用简单的现实答案来解释。在软件设计领域,它非常重要。

    看看你家,学校,教堂的任何一扇门…任何建筑物。

    想象一些门的危险就在底部(所以你必须弯腰才能与打开或关闭的门互动)。

    或者其他人刚好在左上角(所以,一些矮人,残疾人,或者凯文·哈特不会发现非常有趣和有用的门)。

    所以设计是关键词,创建程序给别人可以开发/使用它。

    Interfaces所做的是让其他大型项目中的初级/高级开发人员轻松完成工作[1],因此每个人都知道他们在做什么,而没有其他人的帮助,因此您可以尽可能顺利地工作(理论上)。

    (1)如何?。通过暴露价值的形状。所以您实际上不需要文档,因为代码本身是自解释的(很棒)。

    这个答案不是针对特定语言的,而是针对概念驱动的(毕竟,人类通过编写代码来创建工具)。


    下面是一个简单的例子来说明当您编程一个航班预订系统时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //This interface is very flexible and abstract
        addPassenger(Plane seat, Ticket ticket);

    //Boeing is implementation of Plane
        addPassenger(Boeing747 seat, EconomyTicket ticket);
        addPassenger(Cessna, BusinessClass ticket);


        addPassenger(J15, E87687);