为什么我似乎无法掌握界面?
有人可以帮我揭开界面的神秘面纱,还是给我指出一些很好的例子?我一直在这里和那里看到接口弹出,但是我从来没有真正接触过接口或者何时使用它们的好解释。
我在谈论接口与抽象类的上下文中的接口。
解决方案
接口是一种强制对象实现某种功能的方法,而不必使用继承(这会导致代码高度耦合,而不是使用接口可以实现的松散耦合)。
接口描述的是功能,而不是实现。
最简单的答案是接口定义类可以做什么。这是一个"合同",说明班级将能够执行该操作。
Public Interface IRollOver Sub RollOver() End Interface Public Class Dog Implements IRollOver Public Sub RollOver() Implements IRollOver.RollOver Console.WriteLine("Rolling Over!") End Sub End Class Public Sub Main() Dim d as New Dog() Dim ro as IRollOver = TryCast(d, IRollOver) If ro isNot Nothing Then ro.RollOver() End If End Sub
基本上,我们可以保证Dog类始终能够翻转,只要它继续实现该接口即可。如果猫曾经拥有过RollOver()的能力,它们也可以实现该接口,并且当我们要求Cat和Cat进行RollOver()时,我们可以同等地对待Dog和Cat。
接口使我们可以针对"描述"而不是类型进行编程,从而可以更轻松地关联软件的元素。
这样想:我们想与旁边的多维数据集中的某人共享数据,因此我们拔出了闪光棒并复制/粘贴。你走到隔壁,那个家伙说:"那是USB吗?"然后你说是的全部。闪存棒的大小无关紧要,制造商所关心的也不是它是USB。
以同样的方式,接口使我们可以简化开发工作。使用另一个类比,想象我们想创建一个虚拟地涂漆汽车的应用程序。我们可能会有这样的签名:
public void Paint(Car car, System.Drawing.Color color)...
这将一直有效,直到客户说"现在我要给卡车涂油漆"为止,这样我们就可以这样做:
public void Paint (Vehicle vehicle, System.Drawing.Color color)...
这将扩大应用范围……直到客户说"现在我要粉刷房屋!"我们从一开始就可以做的就是创建一个接口:
public interface IPaintable{ void Paint(System.Drawing.Color color); }
...并将其传递给例程:
public void Paint(IPaintable item, System.Drawing.Color color){ item.Paint(color); }
希望这是一个很简单的解释,但希望能引起注意。
简而言之:接口是定义了方法但其中没有实现的类。相反,抽象类具有一些已实现的方法,但不是全部。
当我们开车开朋友的车时,我们或者多或者少会知道该怎么做。这是因为常规汽车都具有非常相似的界面:方向盘,踏板等。可以将此接口视为汽车制造商与驾驶员之间的合同。作为驾驶员(从软件角度来说,用户是界面的用户/客户端),我们无需学习其他汽车的详细信息就能驾驶它们:例如,我们所需要知道的就是转动方向盘就能转车。作为汽车制造商(以软件的形式提供接口实现的提供商),我们有一个清晰的主意,即新车应该拥有什么以及应该如何表现,以便驾驶员无需过多的额外培训即可使用它们。该合同是软件设计人员所说的解耦(用户与提供者脱钩)-客户端代码是使用接口而不是接口的特定实现,因此不需要知道实现对象的详细信息接口。
接口在类和调用它的代码之间建立契约。它们还允许我们具有实现相同接口的相似类,但执行不同的操作或者事件,而不必知道我们实际在使用哪个类。作为示例,这可能更有意义,所以让我在这里尝试一个。
假设我们有一对名为"狗","猫"和"老鼠"的课程。这些类中的每一个都是Pet,从理论上讲,我们可以从另一个称为Pet的类继承它们,但这就是问题所在。宠物本身不会做任何事情。你不能去商店买宠物。我们可以去买狗或者猫,但是宠物是一个抽象的概念,而不是具体的。
因此,我们知道宠物可以做某些事情。他们可以睡觉或者吃饭等。因此,我们定义了一个称为IPet的接口,它看起来像这样(Csyntax)
public interface IPet { void Eat(object food); void Sleep(int duration); }
Dog,Cat和Mouse类中的每一个都实现IPet。
public class Dog : IPet
因此,现在每个类都必须拥有自己的Eat and Sleep实现。是的,我们有合同...现在有什么意义。
接下来,假设我们要创建一个名为PetStore的新对象。而且这不是一个很好的PetStore,因此他们基本上只是向我们出售随机宠物(是的,我知道这是一个虚构的示例)。
public class PetStore { public static IPet GetRandomPet() { //Code to return a random Dog, Cat, or Mouse } } IPet myNewRandomPet = PetStore.GetRandomPet(); myNewRandomPet.Sleep(10);
问题是我们不知道它将是哪种宠物。由于有了该界面,尽管我们知道它会吃什么还是睡觉。
因此,这个答案可能根本没有用,但是总体的想法是,接口使我们可以完成诸如依赖注入和控制反转之类的精巧东西,在其中我们可以获取一个对象,并提供了一个明确定义的列表,这些对象可以使对象真正地完成工作知道该对象的具体类型是什么。
将接口视为合同。当一个类实现一个接口时,它实质上是同意遵守该合同的条款。作为消费者,我们只关心拥有的对象可以履行其合同义务。他们的内部运作方式和细节并不重要。
接口使我们可以以通用方式针对对象进行编码。例如,假设我们有一种发送报告的方法。现在说我们有一个新要求,即需要编写新报告的地方。如果我们可以重用已经正确编写的方法,那将是很好的选择?接口使操作变得简单:
interface IReport { string RenderReport(); } class MyNewReport : IReport { public string RenderReport() { return "Hello World Report!"; } } class AnotherReport : IReport { public string RenderReport() { return "Another Report!"; } } //This class can process any report that implements IReport! class ReportEmailer() { public void EmailReport(IReport report) { Email(report.RenderReport()); } } class MyApp() { void Main() { //create specific "MyNewReport" report using interface IReport newReport = new MyNewReport(); //create specific "AnotherReport" report using interface IReport anotherReport = new AnotherReport(); ReportEmailer reportEmailer = new ReportEmailer(); //emailer expects interface reportEmailer.EmailReport(newReport); reportEmailer.EmailReport(anotherReport); } }
这是我经常使用的与数据库相关的示例。假设我们有一个对象和一个容器对象(如列表)。让我们假设有时我们可能想按特定顺序存储对象。假定序列与数组中的位置无关,而是对象是较大对象集的子集,并且序列位置与数据库sql过滤相关。
为了跟踪自定义序列的位置,可以使对象实现自定义接口。定制界面可以调解维护此类序列所需的组织工作。
例如,我们感兴趣的序列与记录中的主键无关。使用对象实现接口后,我们可以说myObject.next()或者myObject.prev()。
这是一个相当"漫长的"主题,但让我尝试简单一点。
接口是-如"它们命名为"合约。但是,请不要理会这个词。
理解它们的最佳方法是通过某种伪代码示例。这就是我很久以前对它们的理解。
假设我们有一个处理消息的应用程序。消息包含一些内容,例如主题,文本等。
因此,我们可以编写MessageController来读取数据库并提取消息。直到我们突然听到也即将实施传真之前,这非常高兴。因此,我们现在必须阅读"传真"并将其作为消息处理!
这很容易变成Spagetti代码。因此,我们要做的不是让MessageController仅仅控制" Messages",而是使其能够与名为IMessage的接口一起使用(我只是常见用法,但不是必需的)。
IMessage界面包含一些基本数据,我们需要确保它们能够像这样处理Message。
因此,当我们创建EMail,Fax,PhoneCall类时,可以使它们实现名为IMessage的接口。
因此,在MessageController中,我们可以有一个像这样的方法:
private void ProcessMessage(IMessage oneMessage) { DoSomething(); }
如果我们没有使用过Interfaces,则必须具备以下条件:
private void ProcessEmail(Email someEmail); private void ProcessFax(Fax someFax); etc.
因此,通过使用公共接口,我们只需确保ProcessMessage方法将能够使用它,而不管它是传真,通过电子邮件发送电话还是通过电话发送等等。
为什么或者如何?
因为该接口是一个约定,它指定了某些内容,所以我们必须遵守(或者实现)该功能才能使用它。将其视为徽章。如果对象"传真"没有IMessage接口,则ProcessMessage方法将无法使用该接口,它将为我们提供无效的类型,因为我们正在将传真传递给需要IMessage的方法目的。
你明白这一点吗?
尽管是真正的对象类型,也可以将接口视为可以使用的方法和属性的"子集"。如果原始对象(传真,电子邮件,电话呼叫等)实现了该接口,则可以在需要该接口的方法之间安全地传递它。
那里隐藏着更多的魔力,我们可以将接口投射回其原始对象:
传真myFax =(Fax)SomeIMessageThatIReceive;
.NET 1.1中的ArrayList()有一个很好的接口,称为IList。如果我们有一个IList(非常"泛型"),则可以将其转换为ArrayList:
ArrayList ar = (ArrayList)SomeIList;
野外有成千上万的样本。
诸如ISortable,IComparable等接口定义了必须在类中实现的方法和属性,以实现该功能。
为了扩展我们的示例,如果Type为IMessage,则可以在同一列表中同时包含一个Emails,Fax,PhoneCall的List <>,但是如果对象只是Email,Fax等,则无法将它们全部在一起。
如果我们想对对象进行排序(或者枚举),则需要它们实现相应的接口。在.NET示例中,如果我们有一个"传真"对象列表,并且希望能够使用MyList.Sort()对它们进行排序,则需要使传真类如下:
public class Fax : ISorteable { //implement the ISorteable stuff here. }
我希望这会给我们提示。其他用户可能会发布其他好的示例。祝你好运!并拥抱界面的力量。
警告:并不是所有关于接口的东西都好,它们有一些问题,OOP纯粹主义者将对此发动战争。我将留在一边。 Interfce的一个缺点(至少在.NET 2.0中)是我们不能拥有PRIVATE成员,也不能受到保护,它必须是公共的。这是有道理的,但是有时我们希望我们可以简单地将内容声明为私有或者受保护。
假设我们使用静态类型的面向对象语言引用接口,则主要用途是断言类遵循特定的约定或者协议。
说我们有:
public interface ICommand { void Execute(); } public class PrintSomething : ICommand { OutputStream Stream { get; set; } String Content {get; set;} void Execute() { Stream.Write(content); } }
现在我们有了一个可替换的命令结构。可以将实现IExecute的类的任何实例存储在某种列表中,比如说实现IEnumerable的列表,并且我们可以遍历该实例并执行每个实例,知道每个对象将做正确的事。我们可以通过实现CompositeCommand来创建复合命令,该命令将具有自己的要运行的命令列表,或者通过LoopingCommand来重复运行一组命令,那么我们将拥有大部分简单的解释器。
当我们可以将一组对象简化为它们都具有的共同行为时,我们可能有原因要提取一个接口。另外,有时我们可以使用接口来防止对象意外地侵入该类的关注范围;例如,例如,我们可以实现一个接口,该接口仅允许客户端检索而不是更改对象中的数据,并且大多数对象仅接收对检索接口的引用。
当界面相对简单且很少假设时,界面才能发挥最佳作用。
查找Liskov替代原理,以更充分地理解这一点。
某些静态类型的语言(例如C ++)不支持将接口作为一等概念,因此我们可以使用纯抽象类创建接口。
更新
由于我们似乎是在询问抽象类与接口,因此,我偏爱的过于简化了:
- 接口定义功能和特征。
- 抽象类定义核心功能。
通常,在构建抽象类之前,我会进行提取接口重构。如果我认为应该有一个创造合同(特别是,子类始终应支持特定类型的构造函数),则我更有可能构建一个抽象类。但是,我很少在C#/ java中使用"纯"抽象类。我更有可能用至少一个包含有意义行为的方法来实现一个类,并使用抽象方法来支持该方法调用的模板方法。然后,抽象类是行为的基础实现,所有具体的子类都可以利用它,而无需重新实现。
接口需要任何实现它们的类来包含接口中定义的方法。
目的是使我们不必知道类中的代码即可知道它是否可以用于特定任务。例如,Java中的Integer类实现了可比较的接口,因此,如果仅看到方法标头(公共类String实现Comparable),则将知道它包含一个compareTo()方法。
好的,这是关于抽象类与接口的关系...
从概念上讲,抽象类将用作基类。它们本身经常已经提供了一些基本功能,并且子类必须提供其自己的抽象方法的实现(这些方法在抽象基类中未实现)。
接口通常用于将客户端代码与特定实现的细节分离。同样,有时可以在不更改客户端代码的情况下切换实现,这使得客户端代码更加通用。
在技术层面上,很难在抽象类和接口之间划清界限,因为在某些语言(例如C ++)中没有语法上的区别,或者因为我们还可以将抽象类用于去耦或者泛化的目的。可以将抽象类用作接口,因为根据定义,每个基类都定义了其所有子类都应遵循的接口(即,应该可以使用子类代替基类)。
除了在编程语言中具有的功能接口之外,当将设计思想表达给其他人时,它们也是强大的语义工具。
具有精心设计的接口的代码库突然变得很容易讨论。 "是的,我们需要一个CredentialsManager来注册新的远程服务器。" "将PropertyMap传递给ThingFactory以获取工作实例。"
能够用一个单词解决一个复杂的问题非常有用。
接口也是多态性的关键,多态性是" OOD的三个支柱"之一。
有人在上面提到了这一点,多态性只是意味着给定的类可以采用不同的"形式"。意思是,如果我们有两个类" Dog"和" Cat",并且都实现了接口" INeedFreshFoodAndWater"(呵呵),则代码可以执行以下操作(伪代码):
INeedFreshFoodAndWater[] array = new INeedFreshFoodAndWater[]; array.Add(new Dog()); array.Add(new Cat()); foreach(INeedFreshFoodAndWater item in array) { item.Feed(); item.Water(); }
它之所以强大是因为它允许我们抽象地对待不同类别的对象,并且可以执行诸如使对象之间的耦合松散等操作。
简单的答案:接口是一堆方法签名(+返回类型)。当一个对象说它实现了一个接口时,我们就知道它公开了那套方法。
在Java中使用接口而不是抽象类的一个很好的理由是,子类不能扩展多个基类,但是它可以实现多个接口。
Java不允许多重继承(出于很好的原因,请查找可怕的菱形),但是如果我们想让类提供几组行为怎么办?假设我们希望任何使用它的人都知道它可以序列化,并且可以在屏幕上绘制自身。答案是实现两个不同的接口。
由于接口不包含自己的实现,也不包含实例成员,因此可以毫无疑问地在同一类中实现多个接口,这是安全的。
不利的一面是,我们将不得不在每个类中分别实现。因此,如果层次结构很简单,并且实现的某些部分对于所有继承类都应该是相同的,则使用抽象类。
就像其他人在这里说过的那样,接口定义协定(使用接口的类将"看起来"如何),抽象类定义共享的功能。
让我们看看代码是否有帮助:
public interface IReport { void RenderReport(); // this just defines the method prototype } public abstract class Reporter { protected void DoSomething() { // this method is the same for every class that inherits from this class } } public class ReportViolators : Reporter, IReport { public void RenderReport() { // some kind of implementation specific to this class } } public class ClientApp { var violatorsReport = new ReportViolators(); // the interface method violatorsReport.RenderReport(); // the abstract class method violatorsReport.DoSomething(); }
接口是一种仍然以强类型和多态方式实现约定的方式。
NET中的IDisposable是一个很好的真实示例。实现IDisposable接口的类强制该类实现Dispose()方法。如果该类未实现Dispose(),则在尝试构建时会出现编译器错误。此外,此代码模式:
using (DisposableClass myClass = new DisposableClass()) { // code goes here }
当执行退出内部块时,将导致myClass.Dispose()自动执行。
但是,这很重要,对于Dispose()方法应该执行的操作没有任何强制执行。我们可以让Dispose()方法从文件中选择随机配方,然后通过电子邮件将其发送到通讯组列表,编译器不在乎。 IDisposable模式的目的是使清理资源更加容易。如果类的实例将保留在文件句柄上,则IDisposable可以很容易地将释放和清理代码集中在一个位置,并且可以促进一种使用风格,以确保始终发生释放。
这就是界面的关键。它们是简化编程约定和设计模式的一种方式。如果使用得当,它可以开发出更简单的自记录代码,这些代码更易于使用,维护和更正确。
接口是一种减少系统不同部分之间可能耦合的机制。
从.NET角度
- 接口定义是操作和/或者属性的列表。
- 接口方法始终是公共的。
- 接口本身不必是公共的。
当创建实现该接口的类时,必须提供该接口定义的所有方法和属性的显式或者隐式实现。
此外,.net仅具有单一继承,并且接口是对象将方法公开给其他不知道或者不在其类层次结构中的对象的必要条件。这也称为暴露行为。
一个更具体的示例:
考虑一下,我们有许多DTO(数据传输对象),它们具有关于谁最后一次更新以及何时更新的属性。问题在于并非所有DTO都具有此属性,因为它并不总是相关的。同时,我们希望有一种通用机制来确保在提交给工作流时设置了这些属性(如果可用),但是工作流对象应与提交的对象松散耦合。即,提交工作流程方法不应真正了解每个对象的所有细节,并且工作流程中的所有对象不一定都是DTO对象。
// first pass - not maintainable void SubmitToWorkflow(object o, User u) { if( o is StreetMap ) { var map = (StreetMap)o; map.LastUpdated = DateTime.UtcNow; map.UpdatedByUser = u.UserID; } else if( o is Person ) { var person = (Person)o; person.LastUpdated = DateTime.Now; // whoops .. should be UtcNow person.UpdatedByUser = u.UserID; } // whoa - very unmaintainable.
在上面的代码中," SubmitToWorkflow()"必须了解每个对象。此外,该代码与一个庞大的if / else / switch混杂在一起,违反了"不要自己重复"(DRY)原则,并且要求开发人员每次将新对象添加到系统时都要记住复制/粘贴更改。
// second pass - brittle void SubmitToWorkflow(object o, User u) { if( o is DTOBase ) { DTOBase dto = (DTOBase)o; dto.LastUpdated = DateTime.UtcNow; dto.UpdatedByUser = u.UserID; }
稍好一些,但仍然很脆弱。如果我们要提交其他类型的对象,则仍然需要更多的case语句。等等。
// third pass pass - also brittle void SubmitToWorkflow(DTOBase dto, User u) { dto.LastUpdated = DateTime.UtcNow; dto.UpdatedByUser = u.UserID;
仍然很脆弱,并且这两种方法都强加了所有DTO必须实现此属性的约束,我们指出这并不是普遍适用的。有些开发人员可能会尝试编写无所事事的方法,但这听起来很糟糕。我们不希望假装的类支持更新跟踪,但不支持。
接口,它们如何提供帮助?
如果我们定义一个非常简单的接口:
public interface IUpdateTracked { DateTime LastUpdated { get; set; } int UpdatedByUser { get; set; } }
任何需要此自动更新跟踪的类都可以实现该接口。
public class SomeDTO : IUpdateTracked { // IUpdateTracked implementation as well as other methods for SomeDTO }
可以使工作流方法更加通用,更小并且更易于维护,并且无论有多少类实现了该接口(DTO或者其他),该方法都将继续起作用,因为它仅处理该接口。
void SubmitToWorkflow(object o, User u) { IUpdateTracked updateTracked = o as IUpdateTracked; if( updateTracked != null ) { updateTracked.LastUpdated = DateTime.UtcNow; updateTracked.UpdatedByUser = u.UserID; } // ...
- 我们可以注意到,变量
void SubmitToWorkflow(IUpdateTracked updateTracked,User u)
可以保证类型安全,但是在这种情况下似乎不相关。
在我们使用的某些生产代码中,我们具有代码生成功能,可以从数据库定义中创建这些DTO类。开发人员要做的唯一一件事就是必须正确创建字段名称,并用接口装饰类。只要将这些属性称为LastUpdated和UpdatedByUser,它就可以正常工作。
也许我们是在问,如果我的数据库是旧数据库,那怎么办?我们只需要多做一些输入即可;接口的另一个重要功能是它们可以让我们在类之间建立桥梁。
在下面的代码中,我们有一个虚拟的" LegacyDTO",这是一个预先存在的对象,具有类似名称的字段。它实现了IUpdateTracked接口,以桥接现有的但命名不同的属性。
// using an interface to bridge properties public class LegacyDTO : IUpdateTracked { public int LegacyUserID { get; set; } public DateTime LastSaved { get; set; } public int UpdatedByUser { get { return LegacyUserID; } set { LegacyUserID = value; } } public DateTime LastUpdated { get { return LastSaved; } set { LastSaved = value; } } }
我们可能会觉得很酷,但是具有多个属性会不会引起混淆?或者如果已经具有这些属性但它们还意味着其他内容,会发生什么? .net使我们能够显式实现该接口。这意味着IUpdateTracked属性仅在我们使用对IUpdateTracked的引用时才可见。注意声明上没有公共修饰符,并且声明包含接口名称。
// explicit implementation of an interface public class YetAnotherObject : IUpdatable { int IUpdatable.UpdatedByUser { ... } DateTime IUpdatable.LastUpdated { ... }
拥有极大的灵活性来定义类如何实现接口,为开发人员提供了很大的自由度,可以使对象与使用对象的方法脱钩。接口是打破耦合的一种好方法。
接口不仅限于此。这只是一个简化的实际示例,它利用了基于接口的编程的一个方面。
如前所述,其他响应者可以创建采用和/或者返回接口引用而不是特定类引用的方法。如果我需要在列表中查找重复项,则可以编写一个方法,该方法采用并返回一个" IList"(定义在列表上进行的操作的接口),并且我不受限于具体的收集类。
// decouples the caller and the code as both // operate only on IList, and are free to swap // out the concrete collection. public IList<T> FindDuplicates( IList<T> list ) { var duplicates = new List<T>() // TODO - write some code to detect duplicate items return duplicates; }
版本说明警告
如果它是公共接口,那么我们声明我保证接口x看起来像这样!并且一旦交付了代码并发布了界面,就永远不要更改它。一旦使用者代码开始依赖该接口,我们就不想在现场破坏他们的代码。
请参阅此Haacked帖子,以进行良好的讨论。
接口与抽象(基)类
抽象类可以提供实现,而接口则不能。如果遵循某些准则,例如NVPI(非虚拟公共接口)模式,则抽象类在版本控制方面会更加灵活。
值得重申的是,在.net中,一个类只能从单个类继承,但一个类可以实现任意数量的接口。
依赖注入
接口和DI的快速总结是,使用接口可使开发人员编写针对接口编程的代码以提供服务。在实践中,我们可能会遇到许多小型接口和小型类,并且一个想法是,只做一件事而只有一件事的小型类更容易编写和维护。
class AnnualRaiseAdjuster : ISalaryAdjuster { AnnualRaiseAdjuster(IPayGradeDetermination payGradeDetermination) { ... } void AdjustSalary(Staff s) { var payGrade = payGradeDetermination.Determine(s); s.Salary = s.Salary * 1.01 + payGrade.Bonus; } }
简而言之,以上代码段显示的好处是薪水等级确定只是注入到年度加薪调整器中。工资等级的确定方式实际上与本课程无关。测试时,开发人员可以模拟工资等级确定结果,以确保薪资调整员按需工作。测试也很快,因为测试仅测试类,而不测试其他所有东西。
虽然这不是一本DI入门书,但因为有整本书籍专门讨论该主题。上面的例子非常简化。
我遇到了与我们相同的问题,我发现"合同"的解释有些混乱。
如果我们指定一个方法将IEnumerable接口作为参数,则可以说这是一个约定,指定参数必须是从IEnumerable接口继承的类型,因此支持IEnumerable接口中指定的所有方法。但是,如果我们使用抽象类或者普通类,也是如此。从那些类继承的任何对象都可以作为参数传递。无论如何,我们都可以说继承的对象支持基类中的所有公共方法,无论基类是普通类,抽象类还是接口。
具有所有抽象方法的抽象类基本上与接口相同,因此我们可以说接口只是没有实现方法的类。实际上,我们可以从语言中删除接口,而仅使用抽象类,而只使用抽象方法。我认为我们将它们分开的原因是出于语义原因,但出于编码原因,我看不出原因,并发现它令人困惑。
另一个建议是将接口重命名为接口类,因为接口只是类的另一种形式。
在某些语言中,存在细微的差异,该差异允许一个类仅继承一个类,但可以继承多个接口,而在其他语言中,我们可以同时拥有多个接口,但这是另一个问题,我认为不直接相关