我已经看过几次提到这个,我不清楚这是什么意思。你何时以及为何会这样做?
我知道接口有什么作用,但事实上我不清楚这一点让我觉得我错过了正确使用它们。
如果你这样做是这样的:
IInterface classRef = new ObjectWhatever()
你可以使用任何实现IInterface
类吗?你什么时候需要这样做?我唯一能想到的是,如果你有一个方法,你不确定将传递什么对象,除了它实现IInterface
。我想不出你需要多久做一次。
另外,你怎么能写一个接受实现接口的对象的方法?那可能吗?
这里有一些很好的答案可以得到各种关于接口和松散耦合代码,控制反转等等的详细信息。有一些相当令人兴奋的讨论,所以我想借此机会分解一下,以了解界面为何有用。
当我第一次开始接触界面时,我也对它们的相关性感到困惑。我不明白你为什么需要它们。如果我们使用像 Java 或 C#这样的语言,我们已经有了继承,并且我认为接口是一种较弱的继承和思想形式,“为什么要这么麻烦?” 从某种意义上说,我是对的,你可以把接口看作是一种弱的继承形式,但除此之外,我最终将它们理解为一种语言结构,将它们视为一种分类表现出来的共同特征或行为的手段。可能有许多不相关的对象类。
例如 - 假设你有一个 SIM 游戏,并有以下类:
class HouseFly inherits Insect {
void FlyAroundYourHead(){}
void LandOnThings(){}
}
class Telemarketer inherits Person {
void CallDuringDinner(){}
void ContinueTalkingWhenYouSayNo(){}
}
显然,这两个对象在直接继承方面没有任何共同之处。但是,你可以说他们都很讨厌。
假设我们的游戏需要有一些随机的东西 ,当他们吃晚餐时会让游戏玩家烦恼。这可能是HouseFly
或Telemarketer
或两者兼而有之 - 但是如何通过单一功能允许两者?你如何要求每种不同类型的对象以同样的方式 “做他们讨厌的事情”?
要意识到的关键是Telemarketer
HouseFly
和HouseFly
都有一个共同的松散解释行为,即使它们在建模方面没有任何相似之处。那么,让我们创建一个可以实现的接口:
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
界面令人讨厌。通过这种方式,我们可以很容易地让Telemarketers
和HouseFlys
以他们自己的方式烦恼 - 我们只关心我们在DiningRoom
对象中有什么东西是害虫,我们并不关心它是什么,他们可以与其他人没有任何共同之处。
这个非常人为的伪代码示例(拖延的时间比我预期的要长很多)仅仅是为了说明在我们使用接口的时候最终为我提供的那种东西。我为这个例子的愚蠢提前道歉,但希望它有助于你的理解。而且,可以肯定的是,您在此处收到的其他答案确实涵盖了当今设计模式和开发方法中使用接口的全部内容。
我以前给学生的具体例子是他们应该写作
List myList = new ArrayList(); // programming to the List interface
代替
ArrayList myList = new ArrayList(); // this is bad
这些在短程序中看起来完全相同,但如果你继续在程序中使用myList
100 次,你就可以开始看到差异。第一个声明确保您只调用myList
上由List
接口定义的方法(因此没有特定于ArrayList
方法)。如果您已经通过这种方式编程到界面,稍后就可以确定您确实需要
List myList = new TreeList();
而且你只需要改变那个地方的代码。您已经知道,由于您已编程到接口 ,因此更改实现时 ,其余代码不会执行任何操作 。
当你谈论方法参数和返回值时,好处更明显(我认为)。以此为例:
public ArrayList doSomething(HashMap map);
该方法声明将您与两个具体实现( ArrayList
和HashMap
)联系在一起。一旦从其他代码调用该方法,对这些类型的任何更改可能意味着您还必须更改调用代码。编程到接口会更好。
public List doSomething(Map map);
现在返回什么类型的List
或者作为参数传递什么类型的Map
并不重要。您在doSomething
方法中所做的更改不会强制您更改调用代码。
对界面进行编程时说:“我需要这个功能,而我并不关心它来自哪里。”
考虑(在 Java 中) List
接口与ArrayList
和LinkedList
具体类。如果我关心的是我有一个包含多个数据项的数据结构,我应该通过迭代访问,我会选择一个List
(这是 99%的时间)。如果我知道我需要从列表的任何一端插入 / 删除常量时间,我可能会选择LinkedList
具体实现(或者更可能使用Queue接口)。如果我知道我需要通过索引随机访问,我会选择ArrayList
具体类。