我已经多次看到这句话,但我不清楚它的含义。您何时以及为什么要这样做?
我知道接口的作用,但我不清楚这一点让我觉得我错过了正确使用它们的机会。
如果你这样做是不是这样:
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
共享一个共同的松散解释的行为,即使它们在建模方面完全不同。所以,让我们创建一个两者都可以实现的接口:
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
具体类。
对接口进行编程与我们在 Java 或 .NET 中看到的抽象接口完全无关。它甚至不是 OOP 概念。
这意味着不要乱搞对象或数据结构的内部。使用抽象程序接口或 API 与您的数据进行交互。在 Java 或 C# 中,这意味着使用公共属性和方法而不是原始字段访问。对于 C,这意味着使用函数而不是原始指针。
编辑:对于数据库,这意味着使用视图和存储过程而不是直接访问表。
除了消除类之间不必要的耦合之外,使用接口是使代码易于测试的关键因素。通过创建定义类上的操作的接口,您允许想要使用该功能的类能够使用它,而无需直接依赖于您的实现类。如果稍后您决定更改并使用不同的实现,您只需更改实例化实现的代码部分。其余代码不需要更改,因为它取决于接口,而不是实现类。
这在创建单元测试时非常有用。在被测类中,它依赖于接口,并通过构造函数或属性设置器将接口实例注入类(或允许它根据需要构建接口实例的工厂)。该类在其方法中使用提供的(或创建的)接口。当你去写你的测试时,你可以模拟或伪造接口,并提供一个接口来响应单元测试中配置的数据。您可以这样做,因为您的测试类只处理接口,而不是您的具体实现。任何实现接口的类,包括你的模拟类或假类,都可以。
编辑:下面是一篇文章的链接,其中 Erich Gamma 讨论了他的引言,“Program to an interface, not an implementation”。
http://www.artima.com/lejava/articles/designprinciples.html
您应该研究控制反转:
Martin Fowler:控制容器的反转和依赖注入模式
维基百科:控制反转
在这种情况下,您不会这样写:
IInterface classRef = new ObjectWhatever();
你会写这样的东西:
IInterface classRef = container.Resolve<IInterface>();
这将进入 container
对象中基于规则的设置,并为您构造实际对象,它可以是 ObjectWhatever。重要的是,您可以用完全使用另一种对象的规则替换此规则,并且您的代码仍然可以工作。
如果我们不考虑 IoC,您可以编写知道它可以与执行特定操作的对象通信的代码,但不知道是哪种类型的对象或它是如何实现的。
这在传递参数时会派上用场。
至于带括号的问题“另外,您如何编写一个方法来接收实现接口的对象?这可能吗?”,在 C# 中,您只需使用接口类型作为参数类型,如下所示:
public void DoSomethingToAnObject(IInterface whatever) { ... }
这直接插入“与执行特定操作的对象交谈”。上面定义的方法知道对对象的期望,它实现了 IInterface 中的所有内容,但它不关心它是哪种类型的对象,只关心它遵守约定,这就是接口。
例如,您可能熟悉计算器,并且在您的日子里可能使用过不少,但大多数时候它们都是不同的。另一方面,您知道标准计算器应该如何工作,因此您可以全部使用它们,即使您不能使用每个计算器具有的特定功能,而其他计算器都没有。
这就是界面的美妙之处。您可以编写一段代码,它知道它将获取传递给它的对象,它可以期待某些行为。它并不关心它是什么类型的对象,只关心它支持所需的行为。
让我给你一个具体的例子。
我们有一个为 Windows 窗体定制的翻译系统。该系统循环遍历表单上的控件并翻译每个控件中的文本。系统知道如何处理基本控件,例如具有文本属性的控件类型和类似的基本内容,但对于任何基本的东西,它都达不到要求。
现在,由于控件继承自我们无法控制的预定义类,我们可以做以下三件事之一:
为我们的翻译系统构建支持,以具体检测它正在使用哪种类型的控件,并翻译正确的位(维护噩梦) 将支持构建到基类中(不可能,因为所有控件都继承自不同的预定义类) 添加接口支持
所以我们做了nr。 3. 我们所有的控件都实现了 ILocalizable,它是一个接口,它为我们提供了一种方法,能够将“自身”翻译成翻译文本/规则的容器。因此,表单不需要知道它找到了哪种控件,只需知道它实现了特定的接口,并且知道它可以调用一个方法来本地化控件。
接口代码而不是实现与 Java 无关,也与它的接口构造无关。
这个概念在 Patterns / Gang of Four 书中得到了突出,但很可能在此之前就已经存在了。这个概念肯定早在 Java 出现之前就已经存在了。
Java 接口构造的创建是为了帮助实现这个想法(除其他外),人们已经过于关注构造作为意义的中心,而不是最初的意图。然而,这就是我们在 Java、C++、C# 等中拥有公共和私有方法和属性的原因。
这意味着只与对象或系统的公共接口交互。不要担心,甚至不要预测它是如何在内部完成的。不要担心它是如何实现的。在面向对象的代码中,这就是我们有公共与私有方法/属性的原因。我们打算使用公共方法,因为私有方法仅用于在类内部使用。它们构成了类的实现,可以根据需要进行更改,而无需更改公共接口。假设关于功能,每次使用相同的参数调用类上的方法时,都会执行相同的操作并获得相同的预期结果。它允许作者改变类的工作方式和实现,而不会破坏人们与它的交互方式。
而且您可以对接口进行编程,而不是在不使用接口构造的情况下对实现进行编程。您可以对接口进行编程,而不是在没有接口构造的 C++ 中实现。只要两个大型企业系统通过公共接口(合同)进行交互,而不是调用系统内部对象的方法,您就可以更加健壮地集成两个大型企业系统。给定相同的输入参数,期望接口始终以相同的预期方式做出反应;如果实现到接口而不是实现。这个概念在很多地方都有效。
动摇 Java 接口与“程序到接口,而不是实现”的概念有任何关系的想法。他们可以帮助应用这个概念,但它们不是这个概念。
听起来您了解接口的工作原理,但不确定何时使用它们以及它们提供什么优势。以下是一些接口何时有意义的示例:
// 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 应用程序,我经常在早期创建 XML 存储库,这样我就可以在 SQL 数据库准备好之前启动并运行一些东西。数据库准备好后,我编写一个 SQLRepository 来替换 XML 版本。我的其余代码保持不变,因为它仅在接口之外运行。
方法可以接受以下接口:
PrintZipCodes(IZipCodeRepository zipCodeRepository, string state)
{
foreach (ZipCode zipCode in zipCodeRepository.GetZipCodes(state))
{
Console.WriteLine(zipCode.ToString());
}
}
当您拥有一组类似的类时,它使您的代码更具可扩展性和更易于维护。我是一名初级程序员,所以我不是专家,但我刚刚完成了一个需要类似内容的项目。
我从事与运行医疗设备的服务器通信的客户端软件。我们正在开发该设备的新版本,其中包含客户有时必须配置的一些新组件。有两种类型的新组件,它们是不同的,但它们也非常相似。基本上,我必须创建两个配置表单,两个列表类,两个。
我决定最好为每个控件类型创建一个抽象基类,该基类将包含几乎所有的真实逻辑,然后派生类型来处理两个组件之间的差异。但是,如果我必须一直担心类型,则基类将无法对这些组件执行操作(好吧,它们可以有,但每个方法中都会有一个“if”语句或 switch) .
我为这些组件定义了一个简单的接口,所有的基类都与这个接口通信。现在,当我更改某些内容时,它几乎在任何地方都“正常工作”,而且我没有代码重复。
那里有很多解释,但要使它更简单。以 List
为例。可以使用以下方式实现列表:
内部数组 链表 其他实现
通过构建到一个界面,比如说 List
。您只需编码 List 的定义或 List
在现实中的含义。
您可以在内部使用任何类型的实现,例如 array
实现。但是假设您出于某种原因希望更改实现,例如错误或性能。然后您只需将声明 List<String> ls = new ArrayList<String>()
更改为 List<String> ls = new LinkedList<String>()
。
代码中没有其他地方,您是否必须更改其他任何内容?因为其他一切都建立在 List
的定义之上。
如果您使用 Java 编程,JDBC 就是一个很好的例子。 JDBC 定义了一组接口,但对实现只字未提。您的应用程序可以针对这组接口编写。理论上,您选择一些 JDBC 驱动程序,您的应用程序就可以正常工作。如果您发现有更快或“更好”或更便宜的 JDBC 驱动程序或出于任何原因,理论上您可以再次重新配置您的属性文件,并且无需对您的应用程序进行任何更改,您的应用程序仍然可以工作。
我是这个问题的迟到者,但我想在这里提一下,“编程到接口,而不是实现”这一行在 GoF(四人组)设计模式书中进行了一些很好的讨论。
它说,在第。 18:
编程到接口,而不是实现 不要将变量声明为特定具体类的实例。相反,只提交到由抽象类定义的接口。你会发现这是本书中设计模式的一个共同主题。
除此之外,它开始于:
仅根据抽象类定义的接口来操作对象有两个好处: 只要对象遵循客户期望的接口,客户就不会知道他们使用的对象的具体类型。客户仍然不知道实现这些对象的类。客户只知道定义接口的抽象类。
所以换句话说,不要把它写在你的类中,让它有一个用于鸭子的 quack()
方法,然后是一个用于狗的 bark()
方法,因为它们对于类(或子类)的特定实现来说太具体了.相反,使用足以在基类中使用的通用名称编写方法,例如 giveSound()
或 move()
,以便它们可以用于鸭子、狗甚至汽车,然后是您的客户端类可以只说 .giveSound()
而无需考虑是使用 quack()
还是 bark()
甚至在发出要发送到对象的正确消息之前确定类型。
接口编程很棒,它促进了松散耦合。正如@lassevk 提到的,控制反转是一个很好的用途。
此外,请查看 SOLID 主体。 here is a video series
它通过一个硬编码(强耦合示例)然后查看接口,最后进入一个 IoC/DI 工具(NInject)
添加到现有帖子中,当开发人员同时处理单独的组件时,有时对接口进行编码有助于大型项目。您所需要的只是预先定义接口并向它们编写代码,而其他开发人员则向您正在实现的接口编写代码。
即使我们不依赖抽象,对接口进行编程也是有利的。
对接口进行编程迫使我们使用对象的上下文适当的子集。这很有帮助,因为它:
防止我们做不适合上下文的事情,并让我们在未来安全地更改实现。
例如,考虑一个实现 Friend
和 Employee
接口的 Person
类。
class Person implements AbstractEmployee, AbstractFriend {
}
在此人生日的上下文中,我们对 Friend
接口进行编程,以防止将此人视为 Employee
。
function party() {
const friend: Friend = new Person("Kathryn");
friend.HaveFun();
}
在人员工作的上下文中,我们对 Employee
界面进行编程,以防止工作场所界限模糊。
function workplace() {
const employee: Employee = new Person("Kathryn");
employee.DoWork();
}
伟大的。我们在不同的环境中表现得恰到好处,我们的软件运行良好。
在遥远的未来,如果我们的业务改变为与狗一起工作,我们可以相当容易地改变软件。首先,我们创建一个同时实现 Friend
和 Employee
的 Dog
类。然后,我们安全地将 new Person()
更改为 new Dog()
。即使这两个函数都有数千行代码,这个简单的编辑也可以工作,因为我们知道以下情况是正确的:
函数方仅使用 Person 的 Friend 子集。函数工作场所仅使用 Person 的 Employee 子集。类 Dog 实现了 Friend 和 Employee 接口。
另一方面,如果 party
或 workplace
要针对 Person
进行编程,则两者都有特定于 Person
的代码的风险。从 Person
更改为 Dog
将需要我们梳理代码以消除 Dog
不支持的任何特定于 Person
的代码。
寓意:对接口进行编程有助于我们的代码表现得恰当并为变化做好准备。它还使我们的代码准备好依赖于抽象,这带来了更多的优势。
如果我正在编写一个新类 Swimmer
以添加功能 swim()
并且需要使用类的对象,例如 Dog
,并且此 Dog
类实现声明 swim()
的接口 Animal
。
在层次结构的顶部 (Animal
),它非常抽象,而在底部 (Dog
),它非常具体。我对“接口编程”的看法是,当我编写 Swimmer
类时,我想针对该层次结构中最上层的接口编写代码,在这种情况下是一个 Animal
对象。接口没有实现细节,因此使您的代码松散耦合。
实现细节可以随着时间而改变,但是,它不会影响剩余的代码,因为你所交互的只是接口而不是实现。你不在乎实现是什么样的......你所知道的就是会有一个实现接口的类。
它对单元测试也有好处,您可以将自己的类(满足接口要求)注入到依赖于它的类中
短篇小说:一个邮递员被要求一个接一个地回家并收到包含(信件、文件、支票、礼品卡、申请表、情书)的封面,上面写着要投递的地址。
假设没有掩护,让邮递员一个接一个地回家,把所有的东西都收到并交付给其他人,邮递员可能会感到困惑。
所以最好用封面包裹它(在我们的故事中它是界面)然后他会做得很好。
现在邮递员的工作是只接收和交付封面(他不会打扰封面里面的东西)。
创建一个类型为 interface
的类型不是实际类型,而是使用实际类型实现它。
创建接口意味着您的组件可以轻松融入其余代码
我给你一个例子。
你有如下的 AirPlane 界面。
interface Airplane{
parkPlane();
servicePlane();
}
假设您的平面控制器类中有方法,例如
parkPlane(Airplane plane)
和
servicePlane(Airplane plane)
在您的程序中实施。它不会BREAK您的代码。我的意思是,只要它接受参数为 AirPlane
,它就不需要更改。
因为无论实际类型如何,它都会接受任何 Airplane,flyer
、highflyr
、fighter
等。
此外,在一个集合中:
List<Airplane> plane;
// 将带走你所有的飞机。
下面的例子将清楚你的理解。
你有一架战斗机来实现它,所以
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.
}
}
HighFlyer 和其他课程也是如此:
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
,
假设您的 Controller 类是 ControlPlane,如下所示,
public Class ControlPlane{
AirPlane plane;
// so much method with AirPlane reference are used here...
}
神奇的是,您可以根据需要创建任意数量的新 AirPlane
类型实例,并且您无需更改 ControlPlane
类的代码。
您可以添加一个实例...
JumboJetPlane // implementing AirPlane interface.
AirBus // implementing AirPlane interface.
您也可以删除以前创建的类型的实例。
因此,为了做到这一点,接口的优点是我可以将方法的调用与任何特定的类分开。而是创建接口的实例,其中实现是从我选择的实现该接口的任何类中给出的。因此允许我拥有许多类,它们具有相似但略有不同的功能,并且在某些情况下(与接口意图相关的情况)不关心它是哪个对象。
例如,我可以有一个运动界面。可以传递使某物“移动”的方法以及实现移动接口的任何对象(Person、Car、Cat)并被告知移动。如果没有方法,每个人都知道它是类的类型。
想象一下,您有一个可以通过插件扩展的名为“Zebra”的产品。它通过在某个目录中搜索 DLL 来找到插件。它加载所有这些 DLL 并使用反射来查找任何实现 IZebraPlugin
的类,然后调用该接口的方法与插件进行通信。
这使得它完全独立于任何特定的插件类——它不关心这些类是什么。它只关心它们是否满足接口规范。
接口是一种像这样定义可扩展点的方式。与接口对话的代码更松散耦合——事实上,它根本不与任何其他特定代码耦合。它可以与多年后从未见过原始开发人员的人编写的插件互操作。
您可以改为使用具有虚函数的基类 - 所有插件都将从基类派生。但这更具限制性,因为一个类只能有一个基类,而它可以实现任意数量的接口。
C++ 解释。
将接口视为您的类的公共方法。
然后,您可以创建一个“依赖于”这些公共方法的模板,以执行它自己的功能(它使类公共接口中定义的函数调用)。假设这个模板是一个容器,就像一个 Vector 类,它所依赖的接口是一个搜索算法。
任何定义函数/接口 Vector 调用的算法类都将满足“合同”(正如原始回复中所解释的那样)。算法甚至不需要属于同一个基类;唯一的要求是向量所依赖的函数/方法(接口)在您的算法中定义。
所有这一切的重点是,您可以提供任何不同的搜索算法/类,只要它提供了 Vector 所依赖的接口(冒泡搜索、顺序搜索、快速搜索)。
您可能还想设计其他容器(列表、队列),通过让它们满足您的搜索算法所依赖的接口/契约来利用与 Vector 相同的搜索算法。
这可以节省时间(OOP 原则“代码重用”),因为您可以一次又一次地编写算法,而不是一次又一次地针对您创建的每个新对象编写算法,而不会因继承树过度复杂化问题。
至于“错过”事物的运作方式;大时代(至少在 C++ 中),因为这是大多数标准模板库框架的操作方式。
当然,当使用继承和抽象类时,对接口进行编程的方法会发生变化;但原理是一样的,你的公共函数/方法就是你的类接口。
这是一个巨大的话题,也是设计模式的基石原则之一。
在 Java 中,这些具体的类都实现了 CharSequence 接口:
字符缓冲区、字符串、字符串缓冲区、字符串生成器
这些具体的类除了 Object 之外没有共同的父类,因此没有任何东西与它们相关,除了它们每个都与字符数组有关、表示字符数组或操作字符数组有关。例如,String 对象一旦实例化就不能更改其字符,而 StringBuffer 或 StringBuilder 的字符是可以编辑的。
然而,这些类中的每一个都能够适当地实现 CharSequence 接口方法:
char charAt(int index)
int length()
CharSequence subSequence(int start, int end)
String toString()
在某些情况下,过去接受 String 的 Java 类库类已被修改为现在接受 CharSequence 接口。所以如果你有一个 StringBuilder 的实例,而不是提取一个 String 对象(这意味着实例化一个新的对象实例),它可以只传递 StringBuilder 本身,因为它实现了 CharSequence 接口。
对于可以将字符附加到底层具体类对象实例的实例的任何情况,某些类实现的 Appendable 接口具有相同的好处。所有这些具体类都实现了 Appendable 接口:
BufferedWriter、CharArrayWriter、CharBuffer、FileWriter、FilterWriter、LogStream、OutputStreamWriter、PipedWriter、PrintStream、PrintWriter、StringBuffer、StringBuilder、StringWriter、Writer
CharSequence
这样的界面是如此贫乏。我希望 Java 和 .NET 允许接口具有默认实现,这样人们就不会纯粹为了最小化样板代码而削减接口。给定任何合法的 CharSequence
实现,仅使用上述四种方法即可模拟 String
的大部分功能,但许多实现可以通过其他方式更有效地执行这些功能。不幸的是,即使 CharSequence
的特定实现将所有内容都包含在一个 char[]
中并且可以执行许多...
indexOf
这样的操作很快,不熟悉 CharSequence
特定实现的调用者不可能要求它这样做,而不必使用 charAt
来检查每个单独的字符。
接口就像一个合同,您希望您的实现类在其中实现合同(接口)中编写的方法。由于Java不提供多重继承,“编程到接口”是实现多重继承的好方法。
如果您的 A 类已经在扩展其他 B 类,但您希望该 A 类也遵循某些准则或实现某个约定,那么您可以通过“编程到接口”策略来实现。
问: - ...“你能使用任何实现接口的类吗?”答: - 是的。问:- ...“你什么时候需要这样做?”答: - 每次您需要一个实现接口的类时。
注意:我们无法实例化未由类实现的接口 - 是的。
为什么?
因为接口只有方法原型,没有定义(只有函数名,没有它们的逻辑)
AnIntf anInst = new Aclass(); // 只有当 Aclass 实现 AnIntf 时,我们才能这样做。 // anInst 将具有 Aclass 引用。
注意:现在我们可以理解如果 Bclass 和 Cclass 实现相同的 Dintf 会发生什么。
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.
我们拥有:相同的接口原型(接口中的函数名称),并调用不同的实现。
参考书目: Prototypes - wikipedia
为了可扩展性和松散耦合,以前的答案侧重于抽象编程。虽然这些都是非常重要的点,但可读性同样重要。可读性允许其他人(以及你未来的自己)以最小的努力理解代码。这就是可读性利用抽象的原因。
根据定义,抽象比其实现更简单。抽象省略细节以传达事物的本质或目的,但仅此而已。因为抽象更简单,与实现相比,我可以一次在脑海中容纳更多的抽象。
作为一名程序员(使用任何语言),我总是带着一个List
的一般概念四处走动。特别是,List
允许随机访问、重复元素并保持顺序。当我看到这样的声明时:List myList = new ArrayList()
我想,cool,这是一个 List
,它以我理解的(基本)方式使用;我不必再考虑了。
另一方面,我并没有将ArrayList
的具体实现细节放在脑海中。所以当我看到时,ArrayList myList = new ArrayList()
。我认为,呃-哦,这个 ArrayList
必须以 List
接口未涵盖的方式使用。现在我必须跟踪此 ArrayList
的所有用法以了解原因,否则我将无法完全理解此代码。当我发现这个 ArrayList
do 100% 的用法都符合 List
接口时,它变得更加令人困惑。然后我想知道...是否有一些依赖于 ArrayList
实现细节的代码被删除了?实例化它的程序员只是不称职吗?此应用程序是否在运行时以某种方式锁定到该特定实现?一种我不明白的方式?
我现在对这个应用程序感到困惑和不确定,我们所说的只是一个简单的 List
。如果这是一个忽略其接口的复杂业务对象怎么办?那么我的业务领域知识不足以理解代码的用途。
因此,即使我需要在 private
方法中严格使用 List
(如果它发生更改,也不会破坏其他应用程序,而且我可以轻松地找到/替换我的 IDE 中的每个用法),它仍然有助于抽象编程的可读性。因为抽象比实现细节更简单。您可以说对抽象进行编程是遵守 KISS 原则的一种方式。
program to an interface 是 GOF 书中的一个术语。我不会直接说它与 java 接口有关,而是与真正的接口有关。要实现干净的层分离,您需要在系统之间创建一些分离,例如:假设您有一个要使用的具体数据库,您永远不会“编程到数据库”,而是“编程到存储接口”。同样,您永远不会“编程到 Web 服务”,而是会编程到“客户端接口”。这样您就可以轻松地换掉东西。
我发现这些规则对我有帮助:
1.当我们有多种类型的对象时,我们使用java接口。如果我只有一个对象,我不明白这一点。如果某个想法至少有两个具体实现,那么我将使用 java 接口。
2.如果如上所述,您想将外部系统(存储系统)的解耦带到您自己的系统(本地数据库),那么也使用接口。
请注意如何有两种方法来考虑何时使用它们。
对接口的编程允许无缝地更改接口定义的合约的实现。它允许合约和特定实现之间的松散耦合。
IInterface classRef = new ObjectWhatever() 你可以使用任何实现 IInterface 的类吗?你什么时候需要这样做?
看看这个 SE question 作为一个很好的例子。
Why should the interface for a Java class be preferred?
使用接口会影响性能吗?如果有,多少钱?
是的。它将在亚秒内产生轻微的性能开销。但是如果您的应用程序需要动态更改接口的实现,请不要担心性能影响。
如何避免它而不必维护两位代码?
如果您的应用程序需要它们,请不要尝试避免接口的多个实现。在没有接口与一个特定实现紧密耦合的情况下,您可能必须部署补丁才能将一个实现更改为另一个实现。
一个很好的用例:策略模式的实现:
Real World Example of the Strategy Pattern
“程序到接口”意味着不要以正确的方式提供硬代码,这意味着您的代码应该在不破坏先前功能的情况下进行扩展。只是扩展,而不是编辑以前的代码。
对接口进行编码是一种哲学,而不是特定的语言结构或设计模式——它会指导您为了创建更好的软件系统(例如,更具弹性、更可测试、更可扩展、更可扩展、和其他不错的特征)。
它的实际意思是:
===
在跳转到实现和编码(HOW)之前 - 想想什么:
什么黑匣子应该构成你的系统,
每个盒子的责任是什么,
每个“客户端”(即其他盒子之一、第 3 方“盒子”甚至人类)应该与它通信的方式是什么(每个盒子的 API)。
在您了解上述内容之后,继续实施这些框(HOW)。
首先思考盒子是什么以及它的 API 是什么,引导开发人员提炼盒子的责任,并为自己和未来的开发人员标记它的暴露细节(“API”)和隐藏细节(“实施细节”),这是一个非常重要的区别。
一个直接且显而易见的收获是团队可以在不影响总体架构的情况下更改和改进实施。它还使系统更具可测试性(与 TDD 方法相得益彰)。
=== 除了我上面提到的特征之外,你还可以节省很多时间去这个方向。
微服务和 DDD,如果做得好,是“编码到接口”的很好的例子,但是这个概念在从单体到“无服务器”,从 BE 到 FE,从 OOP 到功能等等的每一种模式中都胜出。
我强烈推荐这种方法用于软件工程(我基本上相信它在其他领域也完全有意义)。
另外我在这里看到了很多很好的解释性答案,所以我想在这里给出我的观点,包括我在使用这种方法时注意到的一些额外信息。
单元测试
在过去的两年里,我写了一个爱好项目,但我没有为它写单元测试。在写了大约 50K 行之后,我发现编写单元测试真的很有必要。我没有使用接口(或者很少使用)……当我进行第一次单元测试时,我发现它很复杂。为什么?
因为我必须制作很多类实例,用作类变量和/或参数的输入。所以这些测试看起来更像是集成测试(必须建立一个完整的类“框架”,因为所有这些都被捆绑在一起)。
害怕接口所以我决定使用接口。我担心我必须多次在所有地方(在所有使用的类中)实现所有功能。在某种程度上这是正确的,但是,通过使用继承可以减少很多。
接口和继承的结合我发现这种结合非常好用。我举一个非常简单的例子。
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) 的好处。
不定期副业成功案例分享
BeAnnoying
实现为空操作;此接口可能存在于烦人事物的接口的位置或附加(如果两个接口都存在,则“是烦人的事物”接口应该继承自“可能是烦人的”界面)。使用此类接口的缺点是实现可能会因实现“烦人”数量的存根方法而感到负担。优点是...IPest[]
中的对象是 IPest 引用,您可以调用BeAnnoying()
因为它们具有该方法,而您不能在没有强制转换的情况下调用其他方法。但是,将调用每个对象单独的BeAnnoying()
方法。