Interfaces — What's the point?
接口的原因真的让我难以理解。据我所知,这是一种为不存在的多重继承而进行的工作,这种继承在C中不存在(或者我被告知)。
我看到的是,您预先定义了一些成员和函数,然后必须在类中重新定义这些成员和函数。从而使接口冗余。感觉就像是句法上的垃圾(请不要冒犯我)。垃圾。
在下面给出的示例中,从堆栈溢出时的另一个C接口线程中获取,我只创建一个名为pizza的基类,而不是一个接口。
简单示例(取自不同的堆栈溢出贡献)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public interface IPizza { public void Order(); } public class PepperoniPizza : IPizza { public void Order() { //Order Pepperoni pizza } } public class HawaiiPizza : IPizza { public void Order() { //Order HawaiiPizza } } |
没有人真正用简单的术语解释了界面是如何有用的,所以我将尝试一下(并从Shamim的答案中窃取一个想法)。
让我们来看看比萨饼订购服务的想法。您可以有多种类型的比萨饼,每个比萨饼的常见操作是在系统中准备订单。每个比萨饼都必须准备好,但每个比萨饼都是不同的。例如,当一个馅饼皮比萨被订购时,系统可能需要验证餐厅提供的某些配料,并将那些不需要的配料放在一边。
当你用代码写这个的时候,技术上你可以
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class Pizza() { public void Prepare(PizzaType tp) { switch (tp) { case PizzaType.StuffedCrust: // prepare stuffed crust ingredients in system break; case PizzaType.DeepDish: // prepare deep dish ingredients in system break; //.... etc. } } } |
但是,深碟比萨(用c表示)可能需要在
解决这个问题的正确方法是使用接口。接口声明所有比萨饼都可以准备,但每个比萨饼可以不同地准备。因此,如果您有以下接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public interface IPizza { void Prepare(); } public class StuffedCrustPizza : IPizza { public void Prepare() { // Set settings in system for stuffed crust preparations } } public class DeepDishPizza : IPizza { public void Prepare() { // Set settings in system for deep dish preparations } } |
现在,您的订单处理代码不需要确切知道为了处理配料,订购了什么类型的比萨饼。它只是:
1 2 3 4 5 | public PreparePizzas(IList<IPizza> pizzas) { foreach (IPizza pizza in pizzas) pizza.Prepare(); } |
尽管每种比萨的制作方式不同,但这部分代码并不需要考虑我们要处理的比萨是什么类型,它只知道需要比萨,因此每次调用
关键是接口代表一个契约。任何实现类都必须具有的一组公共方法。从技术上讲,接口只控制语法,即那里有什么方法,它们得到什么参数,它们返回什么。通常它们也封装了语义,尽管这只是通过文档实现的。
然后,您可以拥有一个接口的不同实现,并随意交换它们。在您的示例中,由于每个比萨实例都是一个
python不是静态类型的,因此类型保留并在运行时查找。因此,您可以尝试对任何对象调用
还需要注意的是,通常意义上的接口不一定是C
对我来说,只有当你不再把它们看作是使你的代码更容易/更快地编写的东西时,它们的意义才变得清晰——这不是它们的目的。它们有许多用途:
(这将失去披萨的类比,因为很难想象这种用法)
假设你在屏幕上做一个简单的游戏,它会有你互动的生物。
答:通过在前端和后端实现之间引入松耦合,它们可以使您的代码在将来更容易维护。
你可以先写这个,因为只有巨魔:
1 2 3 4 5 6 7 8 | // This is our back-end implementation of a troll class Troll { void Walk(int distance) { //Implementation here } } |
前端:
1 2 3 4 5 6 |
两周后,市场营销部决定你也需要ORC,因为他们在Twitter上看到了他们,所以你必须做如下的事情:
1 2 3 4 5 6 7 | class Orc { void Walk(int distance) { //Implementation (orcs are faster than trolls) } } |
前端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
你可以看到这是如何变得混乱的。您可以在这里使用一个接口,这样您的前端将被写入一次(这里是重要的一点)测试,然后您可以根据需要插入更多的后端项目:
1 2 3 4 5 6 7 8 9 | interface ICreature { void Walk(int distance) } public class Troll : ICreature public class Orc : ICreature //etc |
前端是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
前端现在只关心接口icreature——它不关心troll或orc的内部实现,只关心它们实现icreature。
从这个角度来看,需要注意的一点是,你也可以很容易地使用一个抽象的生物类,从这个角度来看,这有同样的效果。
你可以把创造物提取到工厂:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
我们的前端会变成:
1 2 3 4 5 6 7 8 | CreatureFactory _factory; void SpawnCreature(creatureType) { ICreature creature = _factory.GetCreature(creatureType); creature.Walk(); } |
前端现在甚至不需要参考实现troll和orc的库(前提是工厂在单独的库中),它不需要知道任何关于它们的信息。
B:假设您的功能只有一些生物才能在您原本相同的数据结构中拥有,例如。
1 2 3 4 5 6 | interface ICanTurnToStone { void TurnToStone(); } public class Troll: ICreature, ICanTurnToStone |
前端可以是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
C:依赖注入的用法
当前端代码和后端实现之间存在非常松散的耦合时,大多数依赖注入框架更容易使用。如果我们以上面的工厂示例为例,让工厂实现一个接口:
1 2 3 | public interface ICreatureFactory { ICreature GetCreature(string creatureType); } |
然后,我们的前端可以通过构造函数(通常为)注入(例如MVC API控制器):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class CreatureController : Controller { private readonly ICreatureFactory _factory; public CreatureController(ICreatureFactory factory) { _factory = factory; } public HttpResponseMessage TurnToStone(string creatureType) { ICreature creature = _factory.GetCreature(creatureType); creature.TurnToStone(); return Request.CreateResponse(HttpStatusCode.OK); } } |
使用DI框架(例如Ninject或AutoFac),我们可以设置它们,以便在运行时,只要构造函数中需要icreatureFactory,就可以创建creatureFactory的实例-这使得我们的代码既美观又简单。
这也意味着,当我们为控制器编写单元测试时,我们可以提供一个模拟的ICreatureFactory(例如,如果具体实现需要DB访问,我们不希望单元测试依赖于此),并轻松地在控制器中测试代码。
D:还有其他用途,例如,您有两个项目A和B,由于"遗留"原因,它们的结构不好,A引用了B。
然后在B中找到需要调用已经在A中的方法的功能。当得到循环引用时,不能使用具体的实现来实现。
您可以在B中声明一个接口,A中的类随后实现。可以将B中的方法传递给实现接口的类的实例,尽管具体对象是A中的类型。
以下是重新解释的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public interface IFood // not Pizza { public void Prepare(); } public class Pizza : IFood { public void Prepare() // Not order for explanations sake { //Prepare Pizza } } public class Burger : IFood { public void Prepare() { //Prepare Burger } } |
上面的例子没有多大意义。您可以使用类(抽象类,如果您希望它只表现为契约)来完成上述所有示例:
1 2 3 4 5 6 7 8 9 10 11 | public abstract class Food { public abstract void Prepare(); } public class Pizza : Food { public override void Prepare() { /* Prepare pizza */ } } public class Burger : Food { public override void Prepare() { /* Prepare Burger */ } } |
您将获得与接口相同的行为。您可以创建一个
更合适的例子是多重继承:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public abstract class MenuItem { public string Name { get; set; } public abstract void BringToTable(); } // Notice Soda only inherits from MenuItem public class Soda : MenuItem { public override void BringToTable() { /* Bring soda to table */ } } // All food needs to be cooked (real food) so we add this // feature to all food menu items public interface IFood { void Cook(); } public class Pizza : MenuItem, IFood { public override void BringToTable() { /* Bring pizza to table */ } public void Cook() { /* Cook Pizza */ } } public class Burger : MenuItem, IFood { public override void BringToTable() { /* Bring burger to table */ } public void Cook() { /* Cook Burger */ } } |
然后您可以将它们全部用作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class Waiter { public void TakeOrder(IEnumerable<MenuItem> order) { // Cook first // (all except soda because soda is not IFood) foreach (var food in order.OfType<IFood>()) food.Cook(); // Bring them all to the table // (everything, including soda, pizza and burger because they're all menu items) foreach (var menuItem in order) menuItem.BringToTable(); } } |
类比的简单解释要解决的问题是:多态的目的是什么?
类比:所以我是建筑工地的前辈。
商人总是在工地上行走。我不知道谁会穿过那些门。但我基本上告诉他们该怎么做。
上述方法的问题在于,我必须:(i)知道谁走在那扇门上,根据门是谁,我必须告诉他们该怎么做。这意味着我必须了解某一特定行业的所有情况。这种方法有相关的成本/收益:
知道该做什么的含义:这意味着,如果卡彭特的代码从:
BuildScaffolding() 更改为BuildScaffold() (即轻微的名称更改),那么我还必须更改调用类(即Foreperson 类),而不是(基本上)只更改一个。对于多态性,您(基本上)只需要进行一次更改即可获得相同的结果。其次,你不必经常问:你是谁?好的,做这个……你是谁?好吧……多态性-它使代码干燥,在某些情况下非常有效:
使用多态性,您可以轻松地添加额外的Tradespeople类,而无需更改任何现有代码。(即第二个实体设计原则:开闭原则)。
解决方案
想象一个场景,不管谁走进门,我都可以说:"工作()",他们做他们擅长的尊重工作:水管工处理管道,电工处理电线。
这种方法的好处是:(i)我不需要确切地知道是谁从那扇门走进来——我只需要知道他们是一种传统,他们可以工作;其次,(ii)我不需要知道任何关于这一特定贸易的信息。传统会处理好的。
因此,不要这样:
1 2 3 | If(electrician) then electrician.FixCablesAndElectricity() if(plumber) then plumber.IncreaseWaterPressureAndFixLeaks() |
我可以这样做:
1 2 3 | ITradesman tradie = Tradesman.Factory(); // in reality i know it's a plumber, but in the real world you won't know who's on the other side of the tradie assignment. tradie.Work(); // and then tradie will do the work of a plumber, or electrician etc. depending on what type of tradesman he is. The foreman doesn't need to know anything, apart from telling the anonymous tradie to get to Work()!! |
有什么好处?
好处是,如果木匠等的具体工作要求发生变化,那么前辈就不需要更改他的代码——他不需要知道或关心。最重要的是,木匠知道work()是什么意思。其次,如果一个新类型的建筑工人来到工作现场,那么工头不需要知道任何有关行业的信息——工头关心的是建筑工人(如焊工、玻璃工、瓦工等)能否完成某些工作。
说明问题和解决方案(有或无接口):无接口(示例1):无接口(示例2):带接口:总结一个界面允许你让这个人去做他们被分配到的工作,而你不知道他们到底是谁,也不知道他们能做什么。这使您可以轻松地添加新的类型(trade),而无需更改现有的代码(从技术上讲,您只需稍微更改一点点),这是OOP方法相对于更实用的编程方法的真正好处。
如果你不明白以上的任何一点,或者如果不清楚,请发表评论,我会努力使答案更好。
比萨饼的例子很糟糕,因为您应该使用一个处理订单的抽象类,例如比萨饼应该覆盖比萨饼类型。
当您有一个共享属性,但是您的类从不同的地方继承,或者当您没有任何可以使用的公共代码时,您可以使用接口。例如,这是可以处理的二手物品,你知道它会被处理,你只是不知道它被处理后会发生什么。
一个接口只是一个契约,它告诉你一个对象可以做些什么,参数和期望的返回类型。
在没有在Python中使用duck类型的情况下,C依赖接口来提供抽象。如果类的依赖项都是具体类型,则不能传入任何其他类型-使用可以传入实现该接口的任何类型的接口。
考虑一下不控制或拥有基类的情况。
以可视化控件为例,在.NET for WinForms中,它们都继承自.NET框架中完全定义的基类控件。
假设您正在创建自定义控件。您希望构建新的按钮、文本框、列表视图、网格、其他内容,并且希望它们都具有一组控件独有的某些功能。
例如,您可能需要一种处理主题化的通用方法,或者一种处理本地化的通用方法。
在这种情况下,您不能"仅仅创建一个基类",因为如果这样做,您必须重新实现与控件相关的所有内容。
相反,您将从button、textbox、listview、gridview等中下降并添加代码。
但这带来了一个问题,您现在如何识别哪些控件是"您的",您如何构建一些代码来表示"对于表单上属于我的所有控件,将主题设置为X"。
输入接口。
接口是一种查看对象、确定对象是否符合某个约定的方法。
您将创建"yourbutton",从按钮下降,并添加对所需所有接口的支持。
这将允许您编写如下代码:
1 2 3 4 5 | foreach (Control ctrl in Controls) { if (ctrl is IMyThemableControl) ((IMyThemableControl)ctrl).SetTheme(newTheme); } |
没有接口这是不可能的,相反,您必须编写这样的代码:
1 2 3 4 5 6 7 8 9 10 |
在这种情况下,您可以(也可能)定义一个比萨饼基类并从中继承。但是,有两个原因使得界面允许您做其他方式无法实现的事情:
类可以实现多个接口。它只是定义类必须具有的特性。实现一系列接口意味着一个类可以在不同的地方实现多个功能。
接口可以在Hogher作用域中定义,而不是类或调用方。这意味着您可以分离功能,分离项目依赖关系,并将功能保留在一个项目或类中,并在其他地方实现它。
2的一个含义是您可以更改正在使用的类,只需要它实现适当的接口。
考虑到您不能在C中使用多重继承,然后再看看您的问题。
接口=收缩,用于松耦合(参见抓取)。
接口实际上是实现类必须遵循的契约,它实际上是我所知道的几乎所有设计模式的基础。
在您的示例中,之所以创建该接口,是因为这样,任何属于披萨的内容(这意味着实现披萨接口)都将被保证已经实现
1 | public void Order(); |
在您提到的代码之后,您可能会有这样的代码:
1 2 3 4 | public void orderMyPizza(IPizza myPizza) { //This will always work, because everyone MUST implement order myPizza.order(); } |
这样您就可以使用多态性,而您所关心的只是对象对order()作出响应。
如果我正在使用API绘制形状,我可能需要使用DirectX、图形调用或OpenGL。所以,我将创建一个接口,它将从您所调用的内容中抽象出我的实现。
所以您调用一个工厂方法:
如果您使用依赖项注入,这就变得更加重要了,因为这样,在XML文件中,您就可以交换实现。
因此,您可能有一个可以导出供一般使用的加密库,另一个只能出售给美国公司,区别在于您更改了配置文件,而程序的其余部分没有更改。
这对.NET中的集合非常有用,因为您应该只使用
只要您对接口进行编码,开发人员就可以更改实际的实现,而程序的其余部分保持不变。
这在单元测试时也很有用,因为您可以模拟整个接口,所以,我不必去数据库,而是去一个只返回静态数据的模拟实现,所以我可以测试我的方法,而不必担心数据库是否因维护而停机。
我在这页上搜索了"作文"一词,但一次也没看到。除了上述答案外,这个答案也非常重要。
在面向对象的项目中使用接口的一个绝对关键的原因是,它们允许您倾向于组合而不是继承。通过实现接口,您可以将实现与应用于它们的各种算法分离开来。
Derek Banas的这篇优秀的"装饰图案"教程(够有趣的是,它还使用了比萨饼作为例子)是一个很有价值的例子:
https://www.youtube.com/watch?V= J40KRWSM4VE
我很惊讶,没有多少文章包含一个接口最重要的原因:设计模式。使用契约更重要,尽管它是对机器代码的语法修饰(老实说,编译器可能只是忽略了它们),抽象和接口对于OOP、人类理解和复杂的系统架构来说是至关重要的。
让我们将披萨的比喻扩展为一顿完整的3道菜的饭。我们仍将拥有所有食品类别的核心
基于这些规范,我们可以实现抽象工厂模式来概念化整个过程,但是使用接口来确保只有基础是具体的。其他的一切都可以变得灵活或鼓励多态性,但是在实现
如果我有更多的时间,我想画一个完整的例子,或者有人可以为我扩展这个例子,但总的来说,C接口将是设计这种类型系统的最佳工具。
当你需要接口时,你会得到它们:)你可以学习例子,但是你需要aha!效果真的得到了它们。
既然您知道了接口是什么,那么只需编写没有它们的代码。迟早你会遇到一个问题,使用接口是最自然的事情。
接口用于在不同的类之间应用连接。例如,您有一个汽车类和一个树类;
1 2 3 | public class Car { ... } public class Tree { ... } |
您希望为这两个类添加一个可燃烧的功能。但每个班级都有自己的燃烧方式。所以你只需做;
1 2 3 4 5 6 7 8 9 | public class Car : IBurnable { public void Burn() { ... } } public class Tree : IBurnable { public void Burn() { ... } } |
下面是一个矩形对象的界面:
1 2 3 4 5 | interface IRectangular { Int32 Width(); Int32 Height(); } |
它所需要的只是实现访问对象宽度和高度的方法。
现在,让我们定义一个方法,它可以处理任何对象,即
1 2 3 4 5 6 7 | static class Utils { public static Int32 Area(IRectangular rect) { return rect.Width() * rect.Height(); } } |
它将返回任何矩形对象的面积。
让我们实现一个矩形的类
1 2 3 4 5 6 7 8 9 10 11 | class SwimmingPool : IRectangular { int width; int height; public SwimmingPool(int w, int h) { width = w; height = h; } public int Width() { return width; } public int Height() { return height; } } |
还有另一类也是矩形的
1 2 3 4 5 6 7 8 9 10 11 | class House : IRectangular { int width; int height; public House(int w, int h) { width = w; height = h; } public int Width() { return width; } public int Height() { return height; } } |
鉴于此,您可以在房屋或游泳池上调用
1 2 3 4 5 6 |
通过这种方式,类可以从任意数量的接口"继承"行为(静态方法)。
接口定义了特定功能的提供者和相应的使用者之间的契约。它将实现与契约(接口)分离。您应该了解面向对象的体系结构和设计。你可以从维基百科开始:http://en.wikipedia.org/wiki/interface_uuu(计算)
这里有很多好的答案,但我想从一个不同的角度来尝试。
您可能熟悉面向对象设计的坚实原则。综上所述:
S-单一责任原则O-开/关原理L-李斯科夫替代原理界面分离原理D-依赖倒置原理
遵循坚实的原则有助于生成干净、分解良好、内聚且松散耦合的代码。鉴于:
"Dependency management is the key challenge in software at every scale" (Donald Knuth)
那么,任何有助于依赖性管理的事情都是一个巨大的胜利。接口和依赖倒置原则确实有助于将代码与具体类的依赖分离开来,因此可以根据行为而不是实现来编写和推理代码。这有助于将代码分解为可在运行时而非编译时组合的组件,也意味着这些组件可以很容易地插入和取出,而无需更改其余代码。
接口尤其有助于依赖倒置原则,其中代码可以被组件化为一组服务,每个服务都由一个接口描述。然后,通过将服务作为构造函数参数传入类,可以在运行时将它们"注入"到类中。如果您开始编写单元测试并使用测试驱动开发,这种技术就变得非常关键。试试看!您将很快理解接口如何帮助将代码分解成可管理的块,这些块可以单独测试。
接口的主要目的是在您和实现该接口的任何其他类之间建立一个契约,从而使您的代码分离并允许扩展。
考虑接口的最简单方法是识别继承的含义。如果类CC继承了类C,这意味着:
继承的这两个功能在某种意义上是独立的;虽然继承同时适用于这两个功能,但也可以不应用第一个功能而应用第二个功能。这很有用,因为允许一个对象从两个或多个不相关的类继承成员要比允许一种类型的事物可替换为多个类型复杂得多。
接口有点像抽象基类,但有一个关键区别:继承基类的对象不能继承任何其他类。相反,一个对象可以实现一个接口,而不影响它继承任何所需类或实现任何其他接口的能力。
这个(在.NET框架中未被充分利用)的一个好特性是,它们使声明性地指示对象可以做的事情成为可能。例如,一些对象将需要数据源对象,它们可以从中按索引检索内容(列表可能是这样),但不需要在其中存储任何内容。其他例程将需要一个数据存储对象,在该对象中,它们可以不按索引(如collection.add)存储内容,但不需要读取任何内容。某些数据类型将允许按索引访问,但不允许写入;其他数据类型将允许写入,但不允许按索引访问。当然,有些人会允许两者兼得。
如果readablebyindex和appendable是不相关的基类,则不可能定义一个类型,该类型既可以传递给需要readablebyindex的对象,也可以传递给需要appendable的对象。一种方法可以尝试通过readablebyindex或appendable派生另一种方法来缓解这种情况;派生类将必须使公共成员同时可用,但警告某些公共成员可能实际上不起作用。微软的一些类和接口可以做到这一点,但这相当棘手。一种更清洁的方法是为不同的目的提供接口,然后让对象为它们实际能做的事情实现接口。如果其中一个具有接口IReadableByIndex和另一个接口IAppendable,那么可以执行其中一个或另一个操作的类可以为它们可以执行的操作实现适当的接口。
对我来说,接口的一个优点是比抽象类更灵活。由于只能继承1个抽象类,但可以实现多个接口,因此对在许多地方继承抽象类的系统所做的更改会出现问题。如果它在100个位置继承,则更改需要对所有100个位置进行更改。但是,通过这个接口,您可以将新的更改放到一个新的接口中,并在需要的地方使用这个接口(interface seq)。从固体)。此外,内存的使用似乎与接口相比要少一些,因为接口示例中的对象在内存中只使用一次,尽管有多少地方实现了接口。
接口也可以菊花链来创建另一个接口。这种实现多个接口的能力使开发人员能够在不必更改当前类功能的情况下向类中添加功能(可靠原则)
o="类应为扩展打开,但为修改关闭"
有人会问一些很好的例子。
另外,在switch语句的情况下,您不再需要在每次希望rio以特定方式执行任务时维护和切换。
在比萨饼的例子中,如果你想做比萨饼,界面就是你所需要的,从那里开始,每个比萨饼都要处理它自己的逻辑。
这有助于减少耦合和循环复杂性。您仍然需要实现逻辑,但在更广阔的领域中,您需要跟踪的内容将更少。
对于每个比萨饼,您可以跟踪特定于该比萨饼的信息。其他比萨有什么并不重要,因为只有其他比萨需要知道。
那么我是否可以再看一看界面,图形界面(winforms/wpf)与界面类似。它只显示最终用户将与之交互的内容。然后,最终用户将不必知道应用程序的设计中包含了什么,而是知道他们可以根据表单上的可用选项来使用它做什么。在OOP视图中,这样做的目的是创建一个结构化的接口,通知其他用户您的DLL库可以使用什么,就像一个保证/契约,字段、方法和属性可以使用(在类中继承)。
我和你一样认为接口是不必要的。这里引用了《cwalina pg 80框架设计指南》中的一句话:"我经常在这里说,接口指定合同。我相信这是一个危险的神话。接口本身并没有指定太多。他和合著者艾布拉姆斯为微软管理了3个.NET版本。他接着说,"契约"是在类的实现中"表达"的。几十年来,很多人都在关注这一点,他们警告微软,在ole/com中最大限度地使用工程范例可能看起来不错,但它的实用性更直接地体现在硬件上。尤其是在80年代和90年代,互操作标准被编成代码。在我们的TCP/IP互联网世界中,我们很少欣赏硬件和软件体操,我们将通过这些技术在主机、小型计算机和微处理器之间"连接"解决方案,而PC机只是其中的一小部分。因此,对接口及其协议的编码使得计算工作得以进行。并且界面被控制。但是,解决X.25与你的申请有什么共同之处,在假期张贴食谱?我已经编码C++和C多年了,我从来没有创建过一次。