我曾多次看到有人提到这一点,但我不清楚这是什么意思。你什么时候,为什么要这么做?

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

如果你要这样做

IInterface classRef = new ObjectWhatever()

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

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


当前回答

c++的解释。

把接口看作是类的公共方法。

然后你可以创建一个“依赖”于这些公共方法的模板来执行它自己的函数(它在类的公共接口中进行函数调用)。假设这个模板是一个容器,就像Vector类一样,它所依赖的接口是一个搜索算法。

任何定义函数/接口Vector调用的算法类都将满足“契约”(就像有人在原始回复中解释的那样)。算法甚至不需要是相同的基类;唯一的要求是向量所依赖的函数/方法(接口)在你的算法中定义。

所有这些的重点是,您可以提供任何不同的搜索算法/类,只要它提供了Vector所依赖的接口(冒泡搜索、顺序搜索、快速搜索)。

您可能还想设计其他容器(列表、队列),通过让它们实现搜索算法所依赖的接口/契约,来利用与Vector相同的搜索算法。

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

至于“错过”事情是如何运作的;这是非常重要的(至少在c++中),因为这是大多数标准模板库框架的运作方式。

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

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

其他回答

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

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

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

你可以使用一个带有虚函数的基类——所有的插件都是从基类派生的。但这有很大的限制,因为一个类只能有一个基类,而它可以实现任意数量的接口。

听起来好像你知道接口是如何工作的,但不确定何时使用它们以及它们提供了什么优势。下面是几个例子,说明界面是有意义的:

// 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等。

大多数外接程序和插件系统都离不开接口。

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

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

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

方法可以接受如下接口:

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

下面是一个简单的示例,用于说明如何对航班预订系统进行编程。

//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); 

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

即使当我们不依赖于抽象时,面向接口编程也是有利的。

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

防止我们做一些不合时宜的事,还有 让我们在将来安全地更改实现。

例如,考虑实现Friend和Employee接口的Person类。

class Person implements AbstractEmployee, AbstractFriend {
}

在这个人的生日的情况下,我们编程到朋友界面,以防止像对待员工一样对待这个人。

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

在这个人的工作环境中,我们对雇员界面进行编程,以防止模糊工作场所的边界。

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

太好了。我们在不同的环境中都有适当的表现,我们的软件运行良好。

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

Function party只使用Person的Friend子集。 函数workplace只使用Person的Employee子集。 类Dog实现了Friend和Employee接口。

另一方面,如果任何一方或工作场所都针对Person进行编程,就会有同时拥有特定于Person的代码的风险。从“人”改为“狗”需要我们梳理代码,删除任何“狗”不支持的“人”特定代码。

寓意:为接口编程可以帮助我们的代码适当地运行,并为更改做好准备。它还使我们的代码能够依赖于抽象,这带来了更多的好处。