提问人:Damien 提问时间:12/21/2008 最后编辑:Dale KDamien 更新时间:5/23/2022 访问量:222962
“编程到接口”是什么意思?
What does it mean to "program to an interface"?
问:
我已经看到过几次提到这一点,但我不清楚这意味着什么。你什么时候以及为什么要这样做?
我知道接口是做什么的,但我不清楚这一点的事实让我觉得我错过了正确使用它们的机会。
如果你要这样做,是这样吗?
IInterface classRef = new ObjectWhatever()
您可以使用任何实现 ?什么时候需要这样做?我唯一能想到的是,如果你有一个方法,并且你不确定除了它实现之外会传递什么对象。我想不出你需要多久这样做一次。IInterface
IInterface
另外,如何编写一个接受实现接口的对象的方法?这可能吗?
答:
您应该研究控制反转:
在这种情况下,你不会这样写:
IInterface classRef = new ObjectWhatever();
你可以这样写:
IInterface classRef = container.Resolve<IInterface>();
这将进入对象中基于规则的设置,并为您构造实际对象,该对象可以是 ObjectWhatever。重要的是,你可以用完全使用另一种类型的对象来替换这个规则,你的代码仍然可以工作。container
如果我们把 IoC 排除在外,你可以编写代码,知道它可以与执行特定操作的对象通信,但不知道是哪种类型的对象或它如何执行操作。
这在传递参数时会派上用场。
至于你括号内的问题“另外,你怎么能写一个方法,接受一个实现接口的对象?这可能吗?“,在 C# 中,您只需将接口类型用于参数类型,如下所示:
public void DoSomethingToAnObject(IInterface whatever) { ... }
这直接插入到“与执行特定操作的对象交谈”中。上面定义的方法知道对象会发生什么,它实现了 IInterface 中的所有内容,但它并不关心它是哪种类型的对象,只关心它遵循协定,这就是接口。
例如,您可能熟悉计算器,并且可能在您的日子里使用过很多计算器,但大多数时候它们都是不同的。另一方面,您知道标准计算器应该如何工作,因此您可以全部使用它们,即使您不能使用每个计算器所具有的特定功能,而其他计算器则没有。
这就是界面的美妙之处。你可以写一段代码,它知道它会把对象传递给它,它可以期待某些行为。它不在乎它是什么类型的对象,只关心它支持所需的行为。
让我举一个具体的例子。
我们有一个定制的 Windows 表单翻译系统。此系统循环访问窗体上的控件,并翻译每个控件中的文本。系统知道如何处理基本控件,例如具有 Text-property 的控件类型以及类似的基本内容,但对于任何基本内容,它都不足。
现在,由于控件继承自我们无法控制的预定义类,因此我们可以执行以下三项操作之一:
- 为我们的转换系统构建支持,以专门检测它正在使用的控件类型,并转换正确的位(维护噩梦)
- 将支持构建到基类中(不可能,因为所有控件都继承自不同的预定义类)
- 添加接口支持
所以我们做了nr。3. 我们所有的控件都实现了 ILocalizable,这是一个接口,它为我们提供了一种方法,能够将“自身”翻译成翻译文本/规则的容器。因此,窗体不需要知道它找到了哪种控件,只需要它实现特定的接口,并且知道有一个方法可以调用它来本地化控件。
评论
对接口进行编程就是说,“我需要这个功能,我不在乎它来自哪里。
考虑(在 Java 中)接口与 和 具体类。如果我只关心我有一个包含多个数据项的数据结构,我应该通过迭代访问这些数据项,我会选择一个(这是 99% 的时间)。如果我知道我需要从列表的两端进行恒定时间插入/删除,我可能会选择具体的实现(或者更有可能的是,使用 Queue 接口)。如果我知道我需要按索引随机访问,我会选择具体类。List
ArrayList
LinkedList
List
LinkedList
ArrayList
评论
当您拥有相似类集时,它使您的代码更具可扩展性且更易于维护。我是一名初级程序员,所以我不是专家,但我刚刚完成了一个需要类似东西的项目。
我从事客户端软件的工作,该软件与运行医疗设备的服务器通信。我们正在开发该设备的新版本,其中包含一些客户有时必须配置的新组件。有两种类型的新组件,它们不同,但它们也非常相似。基本上,我必须创建两个配置表单,两个列表类,两个所有东西。
我决定最好为每个控件类型创建一个抽象基类,该基类将包含几乎所有的实际逻辑,然后派生类型来处理两个组件之间的差异。但是,如果我不得不一直担心类型,基类将无法对这些组件执行操作(好吧,它们可以,但每个方法中都会有一个“if”语句或开关)。
我为这些组件定义了一个简单的接口,所有基类都与此接口通信。现在,当我更改某些内容时,它几乎在任何地方都“有效”,并且我没有代码重复。
为了补充现有的帖子,当开发人员同时处理不同的组件时,有时对接口进行编码有助于大型项目。您只需要预先定义接口并向其编写代码,而其他开发人员则将代码编写到您正在实现的接口。
除了消除类之间不必要的耦合之外,使用接口是使代码易于测试的关键因素。通过创建一个定义类操作的接口,可以允许想要使用该功能的类能够使用它,而无需直接依赖于实现类。如果以后决定更改并使用其他实现,则只需更改实例化实现的代码部分。代码的其余部分不需要更改,因为它依赖于接口,而不是实现类。
这在创建单元测试时非常有用。在被测类中,你让它依赖于接口,并通过构造函数或属性设置器将接口的实例注入到类中(或允许它根据需要构建接口实例的工厂)。该类在其方法中使用提供(或创建)的接口。当您编写测试时,您可以模拟或伪造接口,并提供一个接口来响应单元测试中配置的数据。你可以这样做,因为你的被测试类只处理接口,而不是你的具体实现。任何实现该接口的类,包括你的模拟类或假类,都可以。
编辑:下面是一篇文章的链接,其中 Erich Gamma 讨论了他的名言“编程到接口,而不是实现”。
http://www.artima.com/lejava/articles/designprinciples.html
评论
如果你用 Java 编程,JDBC 就是一个很好的例子。JDBC定义了一组接口,但对实现只字未提。可以针对这组接口编写应用程序。从理论上讲,你选择一些JDBC驱动程序,你的应用程序就可以工作了。如果您发现有更快、“更好”或更便宜的 JDBC 驱动程序,或者出于任何原因,理论上您可以再次重新配置您的属性文件,并且无需对应用程序进行任何更改,您的应用程序仍然可以工作。
评论
我曾经给学生举过一个具体的例子,就是他们应该写
List myList = new ArrayList(); // programming to the List interface
而不是
ArrayList myList = new ArrayList(); // this is bad
这些在短程序中看起来完全相同,但如果你继续在程序中使用 100 次,你就会开始看到差异。第一个声明确保仅调用接口定义的方法(因此没有特定方法)。如果您已经以这种方式编程到界面,那么稍后您可以决定您真正需要myList
myList
List
ArrayList
List myList = new TreeList();
你只需要在那个地方更改你的代码。您已经知道,代码的其余部分不会执行任何会因更改实现而中断的操作,因为您已对接口进行了编程。
当您谈论方法参数和返回值时,好处更加明显(我认为)。举个例子:
public ArrayList doSomething(HashMap map);
该方法声明将您与两个具体实现( 和 )联系起来。一旦从其他代码调用该方法,对这些类型的任何更改都可能意味着您也必须更改调用代码。最好对接口进行编程。ArrayList
HashMap
public List doSomething(Map map);
现在,返回哪种类型,或者作为参数传入哪种类型都无关紧要。在方法中所做的更改不会强制您更改调用代码。List
Map
doSomething
评论
想象一下,您有一个名为“Zebra”的产品,可以通过插件进行扩展。它通过在某个目录中搜索 DLL 来查找插件。它加载所有这些DLL,并使用反射来查找任何实现的类,然后调用该接口的方法与插件进行通信。IZebraPlugin
这使得它完全独立于任何特定的插件类 - 它不在乎类是什么。它只关心它们是否满足接口规范。
接口是定义扩展点的一种方式,如下所示。与接口通信的代码耦合得更松散 - 实际上,它根本不耦合到任何其他特定代码。它可以与多年后由从未见过原始开发人员的人编写的插件进行互操作。
您可以改用带有虚拟函数的基类 - 所有插件都将派生自基类。但这限制要大得多,因为一个类只能有一个基类,而它可以实现任意数量的接口。
对接口进行编程真是太棒了,它促进了松耦合。@lassevk如前所述,控制反转是它的一个很好的用途。
此外,请查看 SOLID 原则。这是一个视频系列
它通过硬编码(强耦合示例),然后查看接口,最后进入 IoC/DI 工具 (NInject)
对于这个问题,这里有一些很好的答案,涉及到关于接口和松散耦合代码、控制反转等的各种细节。有一些相当令人兴奋的讨论,所以我想借此机会分解一下,以理解为什么界面是有用的。
当我第一次开始接触接口时,我也对它们的相关性感到困惑。我不明白你为什么需要它们。如果我们使用像 Java 或 C# 这样的语言,我们已经有了继承,我认为接口是一种较弱的继承形式,并想,“何必呢?从某种意义上说,我是对的,你可以把接口看作是一种弱的继承形式,但除此之外,我最终理解了它们作为一种语言结构的使用,把它们看作是对许多不相关的对象类所表现出的共同特征或行为进行分类的手段。
例如,假设您有一个 SIM 游戏,并且具有以下类:
class HouseFly inherits Insect {
void FlyAroundYourHead(){}
void LandOnThings(){}
}
class Telemarketer inherits Person {
void CallDuringDinner(){}
void ContinueTalkingWhenYouSayNo(){}
}
显然,这两个对象在直接继承方面没有任何共同之处。但是,你可以说它们都很烦人。
假设我们的游戏需要有某种随机的东西,让游戏玩家在吃晚饭时感到恼火。这可以是 a 或 a 或两者兼而有之 -- 但是你如何允许使用一个函数来同时使用两者呢?你如何要求每种不同类型的对象以同样的方式“做他们烦人的事情”?HouseFly
Telemarketer
要意识到的关键是,a 和 共享一个共同的松散解释行为,即使它们在建模方面完全不同。因此,让我们制作一个两者都可以实现的接口: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
这个非常人为的伪代码示例(拖得比我预期的要长得多)只是为了说明最终为我打开电源的那种东西,即我们何时可以使用接口。对于这个例子的愚蠢,我提前道歉,但希望它有助于您的理解。而且,可以肯定的是,您在这里收到的其他发布答案确实涵盖了当今在设计模式和开发方法中使用接口的方方面面。
评论
BeAnnoying
IPest[]
BeAnnoying()
BeAnnoying()
在 Java 中,这些具体类都实现了 CharSequence 接口:
CharBuffer、String、StringBuffer、StringBuilder
除了 Object 之外,这些具体类没有共同的父类,因此除了它们各自与表示或操作的字符数组有关之外,没有任何关联。例如,一旦实例化了 String 对象,就无法更改 String 的字符,而 StringBuffer 或 StringBuilder 的字符可以编辑。
然而,这些类中的每一个都能够适当地实现 CharSequence 接口方法:
char charAt(int index)
int length()
CharSequence subSequence(int start, int end)
String toString()
在某些情况下,过去接受 String 的 Java 类库类已修改为现在接受 CharSequence 接口。因此,如果您有 StringBuilder 的实例,则它可以在实现 CharSequence 接口时传递 StringBuilder 本身,而不是提取 String 对象(这意味着实例化新的对象实例)。
某些类实现的 Appendable 接口对于可以将字符追加到基础具体类对象实例的实例的任何情况都具有大致相同的好处。所有这些具体类都实现了 Appendable 接口:
BufferedWriter、CharArrayWriter、CharBuffer、FileWriter、FilterWriter、LogStream、OutputStreamWriter、PipedWriter、PrintStream、PrintWriter、StringBuffer、StringBuilder、StringWriter、Writer
评论
CharSequence
CharSequence
String
CharSequence
char[]
indexOf
CharSequence
charAt
听起来您了解接口的工作原理,但不确定何时使用它们以及它们提供哪些优势。以下是接口何时有意义的几个示例:
// 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());
}
}
因此,为了正确做到这一点,接口的优点是我可以将方法的调用与任何特定类分开。而是创建一个接口的实例,其中实现是从我选择的实现该接口的任何类中给出的。因此,允许我拥有许多类,这些类具有相似但略有不同的功能,并且在某些情况下(与接口意图相关的情况)不关心它是哪个对象。
例如,我可以有一个移动界面。一种使某些东西“移动”的方法,任何实现移动接口的对象(人、车、猫)都可以被传入并被告知移动。没有方法,每个人都知道它是类的类型。
它对单元测试也有好处,你可以将自己的类(满足接口的要求)注入到依赖于它的类中
如果我正在编写一个新类来添加功能,并且需要使用类的对象,则说,并且该类实现了声明的接口。Swimmer
swim()
Dog
Dog
Animal
swim()
在层次结构的顶部 (),它非常抽象,而在底部 (),它非常具体。我对“接口编程”的思考方式是,当我编写类时,我想针对该层次结构中最上层的接口编写代码,在本例中是一个对象。接口没有实现细节,因此使代码松散耦合。Animal
Dog
Swimmer
Animal
实现细节可以随时间而改变,但是,它不会影响剩余的代码,因为您所处理的只是接口而不是实现。你不在乎实现是什么样的......你所知道的是,将有一个类来实现该接口。
对接口进行编程与抽象接口完全无关,就像我们在 Java 或 .NET 中看到的那样。它甚至不是一个 OOP 概念。
这意味着不要弄乱对象或数据结构的内部。使用抽象程序接口(API)与数据进行交互。在 Java 或 C# 中,这意味着使用公共属性和方法,而不是原始字段访问。对于 C,这意味着使用函数而不是原始指针。
编辑:对于数据库,这意味着使用视图和存储过程,而不是直接访问表。
评论
C++ 解释。
将接口视为类的公共方法。
然后,您可以创建一个“依赖于”这些公共方法的模板,以便执行它自己的功能(它使类中定义的函数调用成为公共接口)。假设这个模板是一个容器,就像一个 Vector 类,它所依赖的接口是一个搜索算法。
任何定义 Vector 调用的函数/接口的算法类都将满足“合约”(正如有人在原始回复中解释的那样)。这些算法甚至不需要属于同一个基类;唯一的要求是 Vector 所依赖的函数/方法(接口)在您的算法中定义。
所有这一切的要点是,你可以提供任何不同的搜索算法/类,只要它提供了 Vector 所依赖的接口(气泡搜索、顺序搜索、快速搜索)。
您可能还想设计其他容器(列表、队列),通过让它们实现搜索算法所依赖的接口/协定,利用与 Vector 相同的搜索算法。
这样可以节省时间(OOP 原则“代码重用”),因为您可以编写一次算法,而不是一次又一次地特定于您创建的每个新对象,而不会使过度生长的继承树的问题过于复杂。
至于“错过”事物的运作方式;大时间(至少在 C++ 中),因为这是大多数标准模板库框架的运行方式。
当然,当使用继承类和抽象类时,对接口进行编程的方法会发生变化;但原理是一样的,你的公共函数/方法就是你的类接口。
这是一个巨大的话题,也是设计模式的基石原则之一。
那里有很多解释,但为了让它更简单。以 .可以使用以下方式实现列表:List
- 内部阵列
- 链表
- 其他实现
通过构建到接口,比如说 .您只编写了 List 的定义或实际含义的代码。List
List
您可以在内部使用任何类型的实现,例如实现。但是,假设您出于某种原因(例如错误或性能)希望更改实现。然后,您只需将声明更改为 .array
List<String> ls = new ArrayList<String>()
List<String> ls = new LinkedList<String>()
在代码中,您不必更改其他任何内容;因为其他一切都建立在 .List
Q: - ...“你能使用任何实现接口的类吗?”
答: - 是的。Q: - ...“你什么时候需要这样做?”
答: - 每次都需要一个实现接口的类。
注意:我们无法实例化不是由类实现的接口 - True。
- 为什么?
- 因为接口只有方法原型,没有定义(只有函数名称,没有它们的逻辑)
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.
我们有什么:相同的接口原型(接口中的函数名称),并调用不同的实现。
参考书目:原型 - 维基百科,自由的百科全书
短篇小说:邮递员被要求回家后回家,并收到封面(信件、文件、支票、礼品卡、申请、情书),上面写着要投递的地址。
假设没有掩护,让邮递员回家后收到所有东西并交付给其他人,邮递员可能会感到困惑。
所以最好用封面包裹它(在我们的故事中是界面),然后他就会做得很好。
现在邮递员的工作是只接收和投递封面(他不会打扰封面里面的东西)。
创建一个不是实际类型的类型,而是使用实际类型实现它。interface
创建到接口意味着您的组件可以轻松适应代码的其余部分
我给你举个例子。
你有如下的 AirPlane 界面。
interface Airplane{
parkPlane();
servicePlane();
}
假设你的 Controller 类中有类似 Planes 的方法
parkPlane(Airplane plane)
和
servicePlane(Airplane plane)
在您的程序中实现。它不会破坏你的代码。
我的意思是,只要它接受参数作为 .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 和其他 clasess 也是如此:
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.
您也可以删除以前创建的类型的实例。
此外,我在这里看到了很多好的解释性答案,所以我想在这里给出我的观点,包括我在使用这种方法时注意到的一些额外信息。
单元测试
在过去的两年里,我写了一个业余爱好项目,我没有为它编写单元测试。在写了大约 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)的好处。
下面是一个简单的示例,用于说明在对航班预订系统进行编程时的情况。
//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);
让我们先从一些定义开始:
由对象操作定义的所有签名的集合称为对象接口
键入 n. 特定接口
上面定义的接口的一个简单示例是所有 PDO 对象方法(如 、 等),作为一个整体,而不是单独使用。这些方法,即它的接口定义了完整的消息集,可以发送到对象的请求。query()
commit()
close()
上面定义的类型是特定的接口。我将使用虚构的形状界面来演示:、、等。draw()
getArea()
getPerimeter()
如果一个对象是数据库类型,我们的意思是它接受数据库接口的消息/请求,等等。对象可以有多种类型。只要数据库对象实现其接口,就可以使它为形状类型,在这种情况下,这将是子类型。query()
commit()
许多对象可以具有许多不同的接口/类型,并以不同的方式实现该接口。这允许我们替换对象,让我们选择要使用的对象。也称为多态性。
客户端将只知道接口,而不知道实现。
因此,从本质上讲,对接口的编程将涉及创建某种类型的抽象类,例如仅指定接口,即 、 等。然后让不同的具体类实现这些接口,例如 Circle 类、Square 类、Triangle 类。 因此,编程到接口而不是实现。Shape
draw()
getCoordinates()
getArea()
我是这个问题的后来者,但我想在这里提一下,“编程到接口,而不是实现”这句话在GoF(Gang of Four)设计模式一书中进行了很好的讨论。
它在第18页说:
编程到接口,而不是实现
不要将变量声明为特定具体类的实例。相反,仅提交到由抽象类定义的接口。你会发现这是本书中设计模式的一个共同主题。
除此之外,它以以下方式开始:
仅根据抽象类定义的接口操作对象有两个好处:
- 只要对象符合客户端期望的接口,客户端就不知道它们使用的特定类型的对象。
- 客户端仍然不知道实现这些对象的类。客户端只知道定义接口的抽象类。
所以换句话说,不要把它写成你的类,这样它就有一个鸭子的方法,然后有一个狗的方法,因为它们对于一个类(或子类)的特定实现来说太具体了。相反,使用足够通用的名称来编写方法,例如 或 ,以便它们可以用于鸭子、狗甚至汽车,然后类的客户端可以说,而不是考虑是否使用 or 甚至确定类型,然后再发出要发送到对象的正确消息。quack()
bark()
giveSound()
move()
.giveSound()
quack()
bark()
接口代码 不是实现与 Java 无关,也与它的接口结构无关。
这个概念在《模式/四人帮》一书中得到了突出,但很可能在那之前就已经存在了。这个概念肯定早在Java出现之前就已经存在了。
Java 接口结构的创建是为了帮助实现这个想法(除其他外),人们过于关注结构作为意义的中心,而不是原始意图。但是,这就是我们在Java,C++,C#等中拥有公共和私有方法和属性的原因。
这意味着只需与对象或系统的公共接口进行交互。不要担心,甚至不要预测它如何在内部做什么。不要担心它是如何实现的。在面向对象的代码中,这就是为什么我们有公共方法/属性与私有方法/属性。我们打算使用公共方法,因为私有方法仅供内部使用,在类中。它们构成了类的实现,可以根据需要进行更改,而无需更改公共接口。假设在功能方面,每次使用相同的参数调用类的方法时,都会以相同的预期结果执行相同的操作。它允许作者更改类的工作方式和实现方式,而不会破坏人们与它的交互方式。
而且,您可以在不使用接口构造的情况下对接口进行编程,而不是对实现进行编程。您可以编程到接口,而不是 C++ 中的实现,后者没有接口构造。您可以更可靠地集成两个大型企业系统,只要它们通过公共接口(协定)进行交互,而不是在系统内部的对象上调用方法。在给定相同的输入参数的情况下,接口应始终以相同的预期方式做出反应;如果实现到接口而不是实现。这个概念在很多地方都有效。
不要以为Java接口与“编程到接口,而不是实现”的概念有任何关系。它们可以帮助应用概念,但它们不是概念。
评论
接口类似于合约,您希望实现类实现在合约(接口)中编写的方法。由于 Java 不提供多重继承,因此“编程到接口”是实现多重继承的好方法。
如果你有一个类 A 已经在扩展其他类 B,但你希望该类 A 也遵循某些准则或实现某个契约,那么你可以通过“编程到接口”策略来实现。
编程到接口允许无缝更改接口定义的合约的实现。它允许合约和特定实现之间的松散耦合。
IInterface classRef = new ObjectWhatever()
您可以使用任何实现 IInterface 的类吗?什么时候需要这样做?
看看这个 SE 问题,看看这个很好的例子。
使用接口会影响性能吗?
如果是这样,多少钱?
是的。它将在亚秒内产生轻微的性能开销。但是,如果您的应用程序需要动态更改接口的实现,请不要担心性能影响。
如何在不维护两位代码的情况下避免它?
如果应用程序需要接口的多个实现,请不要尝试避免它们。如果接口与一个特定实现没有紧密耦合,则可能需要部署修补程序才能将一个实现更改为其他实现。
一个很好的用例:策略模式的实现:
对接口进行编程可能是有利的,即使我们不依赖于抽象。
对接口进行编程迫使我们使用对象的上下文适当的子集。这很有帮助,因为它:
- 防止我们做上下文中不恰当的事情,以及
- 让我们在将来安全地更改实现。
例如,考虑一个实现 和 接口的类。Person
Friend
Employee
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();
}
伟大。我们在不同的环境中表现得很合适,我们的软件运行良好。
在遥远的未来,如果我们的业务转变为与狗一起工作,我们可以很容易地改变软件。首先,我们创建一个实现 和 的类。然后,我们安全地更改为 .即使这两个函数都有数千行代码,这种简单的编辑也会起作用,因为我们知道以下情况是正确的:Dog
Friend
Employee
new Person()
new Dog()
- 函数仅使用 的子集。
party
Friend
Person
- 函数仅使用 的子集。
workplace
Employee
Person
- 类同时实现 和 接口。
Dog
Friend
Employee
另一方面,如果其中任何一个或将针对 进行编程,则两者都存在具有特定代码的风险。从 更改为 将需要我们梳理代码以消除任何不支持的特定代码。party
workplace
Person
Person
Person
Dog
Person
Dog
寓意:对接口进行编程有助于我们的代码适当地运行并为更改做好准备。它还使我们的代码能够依赖于抽象,这带来了更多的优势。
评论
“程序到接口”意味着不要以正确的方式提供硬代码,这意味着您的代码应该在不破坏以前的功能的情况下进行扩展。只是扩展,而不是编辑以前的代码。
我坚信,困难的问题应该用简单的现实世界答案来解释。在软件设计领域,这非常重要。
看看你家里、学校、教堂里的任何一扇门......任何建筑物。
想象一下,有些门的危险就在右下角(所以你必须鞠躬才能与门互动,门是打开或关闭它),
或者其他人只是在左上角(所以,一些侏儒、残疾人或凯文哈特不会觉得这样的门非常有趣和可用)。
所以设计是关键词,创建程序给其他人,人类可以开发/使用它。
这样做是让其他初级/高级开发人员在庞然大物项目中的事情变得容易[1],这样每个人都知道他们在做什么,而几乎没有别人的帮助,这样你就可以尽可能顺利地工作(理论上)。Interfaces
[1]:怎么样?通过暴露价值的形状。所以,你不需要文档,因为代码本身是不言自明的(太棒了)。
这个答案并不是针对特定语言的,而是针对概念驱动的(毕竟,人类是通过编写代码来创造工具的)。
评论
程序到接口是 GOF 书中的一个术语。我不会直接说它与Java接口有关,而是与真实的接口有关。为了实现干净的层分离,您需要在系统之间创建一些分离,例如: 假设你有一个要使用的具体数据库,你永远不会“编程到数据库”,而是“编程到存储接口”。同样,您永远不会“编程到 Web 服务”,而是编程到“客户端接口”。这样你就可以轻松地把东西换掉。
我发现这些规则对我有帮助:
1. 当我们有多种类型的对象时,我们使用 Java 接口。如果我只有一个对象,我看不出有什么意义。如果某个想法至少有两个具体实现,那么我会使用 Java 接口。
2. 如果如上所述,您希望将外部系统(存储系统)解耦到您自己的系统(本地数据库),那么也使用接口。
请注意,有两种方法可以考虑何时使用它们。
前面的答案侧重于为了可扩展性和松耦合而对抽象进行编程。虽然这些是非常重要的点,但可读性同样重要。可读性允许其他人(以及你未来的自己)以最小的努力理解代码。这就是可读性利用抽象的原因。
根据定义,抽象比其实现更简单。抽象省略细节是为了传达事物的本质或目的,但仅此而已。 因为抽象更简单,所以与实现相比,我可以一次在脑海中容纳更多的抽象。
作为一名程序员(任何语言),我总是带着一个大致的想法在脑海中走来走去。特别是,a 允许随机访问、复制元素并保持秩序。当我看到这样的声明时:我认为,很酷,这是一个以我理解的(基本)方式使用的声明;我不必再想了。List
List
List myList = new ArrayList()
List
另一方面,我没有在脑海中随身携带具体的实现细节。所以当我看到时,.我认为,呃,哦,这必须以界面未涵盖的方式使用。现在我必须追踪它的所有用法才能理解原因,否则我将无法完全理解这段代码。当我发现 100% 的用法确实符合接口时,它变得更加令人困惑。然后我就想知道......是否有一些依赖于实现细节的代码被删除了?实例化的程序员只是无能吗?此应用程序是否在运行时以某种方式锁定在该特定实现中?一种我不明白的方式?ArrayList
ArrayList myList = new ArrayList()
ArrayList
List
ArrayList
ArrayList
List
ArrayList
我现在对这个应用程序感到困惑和不确定,我们谈论的只是一个简单的.如果这是一个忽略其接口的复杂业务对象,该怎么办?那么我对业务领域的了解不足以理解代码的目的。List
因此,即使我需要严格按照方法进行操作(如果它发生变化,则不会破坏其他应用程序,并且我可以轻松找到/替换 IDE 中的每个用法),它仍然有利于编程为抽象的可读性。因为抽象比实现细节更简单。可以说,对抽象进行编程是遵循 KISS 原则的一种方式。List
private
评论
对接口进行编码是一种哲学,而不是特定的语言结构或设计模式——它指导你遵循什么步骤的正确顺序,以便创建更好的软件系统(例如,更具弹性、更可测试、更可扩展、更可扩展和其他不错的特征)。
它的实际含义是:
===
在跳到实现和编码(HOW)之前,请考虑一下WHAT:
- 你的系统应该由哪些黑匣子组成,
- 每个箱子的责任是什么,
- 每个“客户端”(即其他盒子之一、第三方“盒子”,甚至人类)应该如何与它(每个盒子的 API)通信。
完成上述操作后,继续实现这些框(HOW)。
首先考虑什么是盒子以及它的 API 是什么,这会导致开发人员提炼出盒子的责任,并为自己和未来的开发人员标记其公开细节(“API”)和隐藏细节(“实现细节”)之间的区别,这是一个非常重要的区别。
一个直接且容易注意到的收益是,团队可以在不影响总体架构的情况下更改和改进实现。它还使系统更具可测试性(它与 TDD 方法相得益彰)。
===
除了我上面提到的特征之外,您还可以节省大量时间朝这个方向发展。
如果做得好,微服务和 DDD 是“编码到接口”的一个很好的例子,但是这个概念在从单体到“无服务器”、从 BE 到 FE、从 OOP 到功能等各种模式中都获胜。
我强烈推荐这种方法用于软件工程(我基本上相信它在其他领域也完全有意义)。
上一个:查找多个平面的平均交点线
下一个:按引用传递与按值传递有什么区别?
评论