"编程到界面"是什么意思?

Dam*_*ien 791 language-agnostic oop interface

我已经看过几次提到这个,我不清楚这是什么意思.你何时以及为何会这样做?

我知道接口有什么作用,但事实上我不清楚这一点让我觉得我错过了正确使用它们.

如果你这样做是这样的:

IInterface classRef = new ObjectWhatever()
Run Code Online (Sandbox Code Playgroud)

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

另外,你怎么能写一个接受实现接口的对象的方法?那可能吗?

Pet*_*yer 1584

这里有一些很好的答案可以得到各种关于接口和松散耦合代码,控制反转等等的详细信息.有一些相当令人兴奋的讨论,所以我想借此机会分解一下,以了解界面为何有用.

当我第一次开始接触界面时,我也对它们的相关性感到困惑.我不明白你为什么需要它们.如果我们使用像Java或C#这样的语言,我们已经有了继承,并且我认为接口是一种较弱的继承和思想形式,"为什么要这么麻烦?" 从某种意义上说,我是对的,你可以把接口看作是一种弱的继承形式,但除此之外,我最终将它们理解为一种语言结构,将它们视为一种分类表现出来的共同特征或行为的手段.可能有许多不相关的对象类.

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

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

class Telemarketer inherits Person {
    void CallDuringDinner(){}
    void ContinueTalkingWhenYouSayNo(){}
}
Run Code Online (Sandbox Code Playgroud)

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

假设我们的游戏需要有一些随机的东西,当他们吃晚餐时会让游戏玩家烦恼.这可能是一个HouseFly或一个Telemarketer或两个 - 但你怎么兼容一个功能?你如何要求每种不同类型的对象以同样的方式"做他们讨厌的事情"?

要意识到的关键是a 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我们现在有两个类,每个类都可以以自己的方式烦人.而且他们不需要从相同的基类派生并分享共同的固有特征 - 他们只需要满足合同IPest- 合同很简单.你只需要BeAnnoying.在这方面,我们可以建模如下:

class DiningRoom {

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

    void ServeDinner() {
        when diningPeople are eating,

        foreach pest in pests
        pest.BeAnnoying();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

ServeDinner当晚餐供应时我们会叫这种方法,我们在餐厅的人应该吃.在我们的小游戏中,当我们的害虫开始工作时 - 每种害虫都被指示通过IPest界面令人讨厌.通过这种方式,我们可以很容易地拥有这两种方式,Telemarketers并且HouseFlys在每种方式中都很烦人 - 我们只关心我们在DiningRoom物体中有什么东西是有害生物,我们并不在乎它是什么,它们什么都没有与其他人共同.

这个非常人为的伪代码示例(拖延的时间比我预期的要长很多)仅仅是为了说明在我们使用接口的时候最终为我提供的那种东西.我为这个例子的愚蠢提前道歉,但希望它有助于你的理解.而且,可以肯定的是,您在此处收到的其他答案确实涵盖了当今设计模式和开发方法中使用接口的全部内容.

  • 封装行为,例如IPest,被称为策略模式,以防万一有人有兴趣跟进更多关于该主题的材料...... (30认同)
  • 有趣的是,你没有指出因为`IPest []`中的对象是IPest引用,你可以调用`BeAnnoying()`因为它们有这个方法,而你不能在没有强制转换的情况下调用其他方法.但是,每个对象都会调用单独的`BeAnnoying()`方法. (8认同)
  • 非常好的解释......我只是在这里说:我从未听说过接口是某种松散的继承机制,但我知道继承被用作定义接口的不良机制(例如,在常规Python中你一直这样做). (4认同)
  • 另一件需要考虑的事情是,在某些情况下,为"可能"烦人的事物设置一个接口可能是有用的,并且有多个对象实现`BeAnnoying`作为无操作; 这个接口可能代替或者除了烦人的东西之外的接口(如果两个接口都存在,"*烦人的东西"接口应该继承"可能*烦人的东西"界面).使用这种接口的缺点是实现可能会因实现"烦人"数量的存根方法而负担沉重.优点是...... (3认同)
  • 这些方法并不是为了表示抽象方法 - 它们的实现与关注接口的问题无关. (3认同)
  • 您可以补充说,这种设计在 DiningRoom 和各种害虫之间足够松散地耦合,您可以**引入新的害虫(想想“类 Covid19 实现 IPest {...};`!”),而当 DiningRoom 时这些害虫甚至不*存在*类已编写!**您不仅不需要*更改*Diningroom,甚至不需要*重新编译*它!对于测试和大型项目非常有用。 (3认同)
  • ...不必要地调用存根方法通常比确定是否应该调用方法更快.这是`IEnumerator <T>`继承`IDisposable`的部分原因.在绝大多数情况下,枚举器的消费者无条件地在其上调用"Dispose"比用户检查是否需要"Dispose"调用更便宜.太糟糕了,.net和Java都不允许指定接口方法的默认实现,因为在很多情况下接口可以从"可选"方法中受益,其中会有... (2认同)
  • 电话推销员与家蝇的关系加一 (2认同)
  • `一种对潜在的许多不相关的对象类别所表现出的共同特征或行为进行分类的方法`这完全是正确的!有时,两个类(或更多类)有共同的联系并不明显。不错的答案:) (2认同)

Bil*_*ard 270

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

List myList = new ArrayList(); // programming to the List interface
Run Code Online (Sandbox Code Playgroud)

代替

ArrayList myList = new ArrayList(); // this is bad
Run Code Online (Sandbox Code Playgroud)

这些在短程序中看起来完全相同,但如果你继续myList在你的程序中使用100次,你可以开始看到差异.第一个声明确保您只调用接口myList定义的方法List(因此没有ArrayList特定的方法).如果您已经通过这种方式编程到界面,稍后就可以确定您确实需要

List myList = new TreeList();
Run Code Online (Sandbox Code Playgroud)

而且你只需要改变那个地方的代码.您已经知道,由于您已编程到接口,因此更改实现时,其余代码不会执行任何操作.

当你谈论方法参数和返回值时,好处更明显(我认为).以此为例:

public ArrayList doSomething(HashMap map);
Run Code Online (Sandbox Code Playgroud)

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

public List doSomething(Map map);
Run Code Online (Sandbox Code Playgroud)

现在,List返回什么样的类型或者Map作为参数传递什么样的类型并不重要.您在doSomething方法内所做的更改不会强制您更改调用代码.

  • @user3014901 您可能出于多种原因想要更改正在使用的列表类型。例如,可能具有更好的查找性能。关键是,如果您对 List 接口进行编程,则以后可以更轻松地将代码更改为不同的实现。 (3认同)

kdg*_*ory 73

对界面进行编程时说:"我需要这个功能,而我并不关心它来自哪里."

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


tva*_*son 37

除了删除类之间不必要的耦合之外,使用接口是使代码易于测试的关键因素.通过创建定义类的操作的接口,您允许希望使用该功能的类能够使用它而无需直接依赖于您的实现类.如果您稍后决定更改并使用其他实现,则只需更改实例实例化的代码部分.其余代码无需更改,因为它取决于接口,而不是实现类.

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

编辑:下面是一篇文章的链接,其中Erich Gamma讨论了他的引用,"程序到界面,而不是实现."

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

  • 请再次阅读本次访谈:Gamma当然是在讨论界面的OO概念,而不是JAVA或C#特殊类(ISomething).问题是,大多数人虽然谈论关键字,但我们现在有很多不受欢迎的接口(ISomething). (3认同)

ang*_*son 35

你应该研究控制反转:

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

IInterface classRef = new ObjectWhatever();
Run Code Online (Sandbox Code Playgroud)

你会写这样的东西:

IInterface classRef = container.Resolve<IInterface>();
Run Code Online (Sandbox Code Playgroud)

这将进入container对象中基于规则的设置,并为您构造实际对象,可以是ObjectWhatever.重要的是,你可以用完全使用其他类型对象的东西替换这个规则,你的代码仍然可以工作.

如果我们将IoC从表中删除,您可以编写代码,该代码知道它可以与执行某些特定操作的对象进行通信,但不能使用哪种类型的对象或如何进行操作.

传递参数时这会派上用场.

至于你的括号问题"另外,你怎么能写一个接受一个实现接口的对象的方法?这可能吗?",在C#中你只需要使用参数类型的接口类型,如下所示:

public void DoSomethingToAnObject(IInterface whatever) { ... }
Run Code Online (Sandbox Code Playgroud)

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

例如,你可能熟悉计算器,并且可能在你的日子里使用了很多,但大多数时候它们都是不同的.另一方面,您知道标准计算器应该如何工作,因此您可以全部使用它们,即使您不能使用每个计算器都没有的特定功能.

这是界面之美.你可以写一段代码,知道它会传递给它的对象,它可以期望某些行为.它并不关心它是什么类型的对象,只是它支持所需的行为.

让我举一个具体的例子.

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

现在,由于控件继承自我们无法控制的预定义类,我们可以做以下三件事之一:

  1. 为我们的翻译系统构建支持,以专门检测它正在使用哪种类型的控件,并翻译正确的位(维护噩梦)
  2. 在基类中构建支持(不可能,因为所有控件都从不同的预定义类继承)
  3. 添加界面支持

所以我们做了nr.3.我们所有的控件都实现了ILocalizable,这是一个为我们提供一种方法的接口,即将"自身"转换为翻译文本/规则容器的能力.因此,表单不需要知道它找到了哪种控件,只需知道它实现了特定的接口,并且知道有一种方法可以调用本地化控件.

  • 为什么在一开始就提到IoC,因为这只会增加更多的混乱. (31认同)

Jon*_*len 34

对接口进行编程与我们在Java或.NET中看到的抽象接口完全无关.它甚至不是OOP概念.

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

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

  • 最佳答案.Gamma在这里给出了类似的解释:http://www.artima.com/lejava/articles/designprinciples.html(参见第2页).他正在提到OO概念,但你是对的:它比那个大. (5认同)

Bil*_*mus 27

接口的代码并非实现与Java及其接口构造无关.

这个概念在"图案/四人帮"中引人注目,但很可能在此之前就已经存在.在Java出现之前,这个概念确实存在.

创建Java接口构造是为了帮助这个想法(除其他外),人们已经过于专注于构造作为意义的中心而不是原始的意图.但是,这就是我们在Java,C++,C#等中使用公共和私有方法和属性的原因.

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

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

摇晃Java接口与"接口程序而不是实现"概念有任何关系的想法.他们可以帮助应用这个概念,但它们不是概念.

  • 第一句话说明了一切。这应该是公认的答案。 (2认同)

Tod*_*ith 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);
}
Run Code Online (Sandbox Code Playgroud)

然后我可以创建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)
}
Run Code Online (Sandbox Code Playgroud)

然后创建JpegImageLoader,GifImageLoader,PngImageLoader等.

大多数加载项和插件系统都在接口之外工作.

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

interface IZipCodeRepository
{
    IList<ZipCode> GetZipCodes(string state);
}
Run Code Online (Sandbox Code Playgroud)

然后我可以创建一个XMLZipCodeRepository,SQLZipCodeRepository,CSVZipCodeRepository等.对于我的Web应用程序,我经常在早期创建XML存储库,这样我就可以在Sql数据库准备好之前启动并运行.数据库准备好后,我编写一个SQLRepository来替换XML版本.我的其余代码保持不变,因为它从接口运行.

方法可以接受以下接口:

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


Ed *_* S. 10

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

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

我决定最好为每种控件类型创建一个抽象基类,它几乎包含所有实际逻辑,然后派生类型来处理两个组件之间的差异.但是,如果我不得不一直担心类型,那么基类就无法对这些组件执行操作(好吧,它们可能有,但是每个方法都会有"if"语句或开关) .

我为这些组件定义了一个简单的接口,所有基类都与这个接口通信.现在,当我改变某些东西时,它几乎"无处不在",我没有代码重复.


Kev*_*nle 8

如果使用Java编程,JDBC就是一个很好的例子.JDBC定义了一组接口,但没有说明实现.您的应用程序可以根据这组接口编写.从理论上讲,您选择了一些JDBC驱动程序,您的应用程序就可以运行.如果您发现有更快或更好或更便宜的JDBC驱动程序或出于任何原因,您可以再次从理论上重新配置属性文件,而无需在应用程序中进行任何更改,您的应用程序仍然可以正常工作.

  • JDBC非常糟糕,需要更换.找另一个例子. (3认同)

dbo*_*nes 8

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

另外,请查看SOLID主体.这是一个视频系列

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


Jat*_*tin 8

那里有很多解释,但要使它更简单.举个例子来说List.可以使用as实现列表:

  1. 内部数组
  2. 链表
  3. 其他实施

通过建立一个界面,比如说List.您只对List的定义或List现实中的含义进行编码.

您可以在内部使用任何类型的实现来说明array实现.但是假设你希望改变实现,出于某种原因说出错误或性能.然后你只需要将声明更改List<String> ls = new ArrayList<String>()List<String> ls = new LinkedList<String>().

在代码中没有其他地方,你是否必须改变其他任何东西; 因为其他一切都是建立在定义上的List.


nop*_*ole 7

我是这个问题的后来者,但是我想在这里提到"程序到界面,而不是实现"这一行在GoF(Gang of Four)设计模式书中有一些很好的讨论.

它说,在第.18:

编程到接口,而不是实现

不要将变量声明为特定具体类的实例.相反,只提交由抽象类定义的接口.你会发现这是本书设计模式的一个共同主题.

以上,它始于:

仅根据抽象类定义的接口操作对象有两个好处:

  1. 只要对象遵循客户期望的接口,客户端就不会意识到它们使用的特定对象类型.
  2. 客户端仍然不知道实现这些对象的类.客户端只知道定义接口的抽象类.

所以换句话说,不要把它写成你的类,以便它有一个quack()ducks 的方法,然后是一个bark()dog 的方法,因为它们对于类(或子类)的特定实现来说太具体了.相反,使用通常足以在基类中使用的名称来编写方法,例如giveSound()move(),以便它们可以用于鸭子,狗,甚至汽车,然后你的类的客户端可以说.giveSound()而不是在发出要发送给对象的正确消息之前考虑是否使用quack()bark()甚至确定类型.


wha*_*ley 6

除了已经选择的答案(以及此处的各种信息性帖子),我强烈建议您获取Head First Design Patterns的副本.这是一个非常简单的阅读,将直接回答您的问题,解释它为什么重要,并向您展示许多编程模式,您可以使用这些模式来使用该原则(和其他).


e11*_*11s 5

要添加到现有帖子中,当开发人员同时处理单独的组件时,有时对接口进行编码有助于大型项目。您需要做的就是预先定义接口并向其中编写代码,而其他开发人员则向要实现的接口编写代码。


Abh*_*ava 5

如果我正在编写一个新类Swimmer来添加功能swim()并且需要使用类的对象 say Dog,并且这个Dog类实现了Animal声明swim().

在层次结构的顶部(Animal),它非常抽象,而在底部(Dog),它非常具体。我对“接口编程”的看法是,当我编写Swimmer类时,我想针对在该层次结构中最上层的接口编写代码,在这种情况下是一个Animal对象。接口不受实现细节的限制,从而使您的代码松散耦合。

实现细节可以随时间改变,但是,它不会影响剩余的代码,因为您所交互的只是接口而不是实现。你不在乎实现是什么样的......你只知道会有一个类来实现接口。


Sha*_*tin 5

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

接口编程迫使我们使用对象的上下文适当的子集。这有帮助,因为它:

  1. 防止我们做上下文不适当的事情,和
  2. 让我们在未来安全地更改实现。

例如,考虑一个Person实现FriendEmployee接口的类。

class Person implements AbstractEmployee, AbstractFriend {
}
Run Code Online (Sandbox Code Playgroud)

在此人生日的背景下,我们对Friend界面进行编程,以防止将人视为Employee.

function party() {
    const friend: Friend = new Person("Kathryn");
    friend.HaveFun();
}
Run Code Online (Sandbox Code Playgroud)

在人员工作的背景下,我们对Employee界面进行编程,以防止工作场所边界模糊。

function workplace() {
    const employee: Employee = new Person("Kathryn");
    employee.DoWork();
}
Run Code Online (Sandbox Code Playgroud)

伟大的。我们在不同的环境中表现得很好,我们的软件运行良好。

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

  1. 函数party仅使用 的Friend子集Person
  2. 函数workplace仅使用 的Employee子集Person
  3. 类同时Dog实现了FriendEmployee接口。

另一方面,如果partyworkplace要针对 进行编程Person,则存在两者都具有Person特定代码的风险。从Personto更改Dog将需要我们梳理代码以消除任何不支持的Person特定代码Dog

寓意:对接口编程有助于我们的代码行为适当并准备好进行更改。它还准备我们的代码依赖于抽象,这带来了更多的优势。