有人可以为我揭开界面的神秘面纱,还是给我一些好的例子?我一直在这里和那里看到界面弹出,但我还没有真正接触到界面的良好解释或何时使用它们.
我在谈论接口与抽象类的上下文中的接口.
接口允许您针对"描述"而不是类型进行编程,这允许您更松散地关联软件的元素.
可以这样想:你想与你旁边的立方体中的某人共享数据,所以你拿出闪存棒并复制/粘贴.你走到隔壁,那家伙说"那是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); }
希望这是有道理的 - 这是一个非常简单的解释,但希望能够触及它的核心.
接口在类和调用它的代码之间建立契约.它们还允许您具有类似的类,这些类实现相同的接口但执行不同的操作或事件,而不必知道您实际使用的是哪个.这可能会更有意义,所以让我在这里尝试一下.
假设您有几个名为Dog,Cat和Mouse的课程.这些类中的每一个都是Pet,理论上你可以从另一个名为Pet的类中继承它们,但这就是问题所在.宠物自己也不做任何事情.你不能去商店购买宠物.你可以去买狗或猫,但宠物是一个抽象的概念,而不是具体的.
所以你知道宠物可以做某些事情.他们可以睡觉,吃饭等等.所以你定义了一个名为IPet的界面,它看起来像这样(C#语法)
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);
问题是你不知道它会是什么类型的宠物.感谢界面,虽然你知道它是什么,它会吃和睡觉.
所以这个答案可能根本没有帮助,但一般的想法是接口让你做一些整洁的东西,比如依赖注入和控制反转,你可以得到一个对象,有一个明确定义的对象可以做的东西列表,而不是真的知道该对象的具体类型是什么.
最简单的答案是接口定义了您的类可以执行的操作.这是一份"合同",表示你的班级将能够做到这一点.
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()的能力,他们也可以实现该界面,并且当他们要求RollOver()时你可以同时对待Dogs和Cats.
当你驾驶朋友的车时,你或多或少知道如何做到这一点.这是因为传统汽车都具有非常相似的界面:方向盘,踏板等.将此界面视为汽车制造商和驾驶员之间的合同.作为一个驱动程序(软件方面的界面的用户/客户),您不需要了解不同车辆的细节就可以驾驶它们:例如,您需要知道的是转动方向盘使得车转.作为汽车制造商(以软件术语实现界面的提供商),您可以清楚地知道您的新车应该具备什么以及它应该如何表现,以便驾驶员可以在不需要额外培训的情况下使用它们.
接口是一种减少系统中不同部分可能不同部分之间耦合的机制.
从.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属性才会显示.请注意声明中没有public修饰符,声明包含接口名称.
// 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 IListFindDuplicates( IList list ) { var duplicates = new List () // 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入门书,因为有完整的书籍专门讨论这个主题; 上面的例子非常简单.
这是一个相当"漫长"的主题,但让我试着简单一点.
接口是 - "他们命名" - 一个合同.但是忘了这个词.
理解它们的最好方法是通过某种伪代码示例.这就是我很久以前对它的理解.
假设您有一个处理消息的应用程序.消息包含一些内容,如主题,文本等.
因此,您编写MessageController以读取数据库并提取消息.在您突然听到传真即将实施之前,这是非常好的.因此,您现在必须阅读"传真"并将其作为消息处理!
这很容易变成Spagetti代码.所以你做的不是使用MessageController而只是控制"Messages",你可以使用一个名为IMessage 的接口(我只是常用,但不是必需的).
您的IMessage界面包含一些您需要的基本数据,以确保您能够处理消息.
因此,当您创建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的方法宾语.
你明白了吗?
将接口视为可用的方法和属性的"子集",尽管是真实的对象类型.如果原始对象(Fax,Email,PhoneCall等)实现了该接口,则可以安全地将其传递给需要该接口的方法.
那里隐藏着更多的魔法,你可以将接口反复回原始对象:
传真myFax =(传真)SomeIMessageThatIReceive;
.NET 1.1中的ArrayList()有一个很好的接口叫做IList.如果您有一个IList(非常"通用"),您可以将其转换为ArrayList:
ArrayList ar = (ArrayList)SomeIList;
野外有成千上万的样本.
像ISortable,IComparable等接口定义了必须在类中实现的方法和属性,以实现该功能.
为了扩展我们的样本,如果Type是IMessage,你可以在同一个List中有一个List <>的电子邮件,传真,PhoneCall,但是如果对象只是电子邮件,传真等,你就无法将它们全部放在一起. .
如果要对对象进行排序(或枚举),则需要它们来实现相应的接口.在.NET示例中,如果您有一个"传真"对象列表并希望能够使用MyList.Sort()对它们进行排序,则需要将传真类设置为:
public class Fax : ISorteable { //implement the ISorteable stuff here. }
我希望这会给你一个暗示.其他用户可能会发布其他好的例子.祝好运!并拥抱INterfaces的力量.
警告:接口并不是一切都很好,它们存在一些问题,OOP纯粹主义者会对此展开一场战争.我会待在一边.Interfce(至少在.NET 2.0中)的一个缺点是,您不能拥有PRIVATE成员,或者受保护,它必须是公共的.这有一定道理,但有时您希望您可以简单地将内容声明为私有或受保护.
除了编程语言中的函数接口之外,它们在向其他人表达设计思想时也是一种强大的语义工具.
具有精心设计的接口的代码库突然更容易讨论."是的,你需要一个CredentialsManager来注册新的远程服务器." "将PropertyMap传递给ThingFactory以获取工作实例."
用一个单词来处理复杂事物的能力非常有用.