你好,我知道这是一个经常出现的问题,但是在阅读了以下帖子之后(什么是接口和类的区别?为什么我应该使用接口而不是在类中直接实现方法?),我仍然很难理解为什么需要使用接口。对于这个基础问题感到抱歉,但是虽然我理解接口和类之间的关系是一种合同,但我似乎无法看到它的实用性。我知道它可以帮助您轻松创建对象,但我感觉我错过了什么。
我已经在这里和整个互联网上阅读了很多关于如何使用接口的帖子,但是有一半的时间我会想,如果你创建一个类并继承它,它不会做同样的事情吗?我在这里错过了什么?
你好,我知道这是一个经常出现的问题,但是在阅读了以下帖子之后(什么是接口和类的区别?为什么我应该使用接口而不是在类中直接实现方法?),我仍然很难理解为什么需要使用接口。对于这个基础问题感到抱歉,但是虽然我理解接口和类之间的关系是一种合同,但我似乎无法看到它的实用性。我知道它可以帮助您轻松创建对象,但我感觉我错过了什么。
我已经在这里和整个互联网上阅读了很多关于如何使用接口的帖子,但是有一半的时间我会想,如果你创建一个类并继承它,它不会做同样的事情吗?我在这里错过了什么?
你开车吗?如果不是,我假设你知道驾驶汽车的一般情况(方向盘,油门,刹车)。下面的回答假定你会开车,且你的车和我的不同。
你曾经开过我的车吗?没有。但是,如果有机会,你能否在无需学习如何开我的车的情况下开我的车?可以。
这也适用于我。我从未开过你的车,但我也能够驾驶它而无需学会如何开车。
为什么呢?因为所有的汽车都有共同的接口。方向盘,右手油门,中间的刹车。没有两辆汽车是完全相同的,但它们都是以让驾驶员和任何汽车之间的互动完全相同的方式构建的。
将此与F16战斗机进行比较。会开车并不意味着你会飞行F16战斗机因为它的接口不同。它没有方向盘,也没有油门/刹车踏板。
主要好处很明显:驾驶员不需要逐个学习如何驾驶每辆汽车。
现在,为了完成这个比喻,汽车的一般概念就是一个接口,而特定的汽车是类。主要好处很明显:你不需要为每个相似的类编写自定义代码。
一个实际的例子
public class BMW
{
public SteeringWheel SteeringWheel { get; set; }
public Pedal Accelerator { get; set; }
public Pedal Brake { get; set; }
}
public class BMWDriver
{
public void ParticipateInRace(BMW myBMW)
{
myBMW.Accelerator.Press();
myBMW.SteeringWheel.TurnLeft();
myBMW.SteeringWheel.TurnRight();
myBMW.Accelerator.Release();
myBMW.Brake.Press();
myBMW.Brake.Release();
}
}
public class Audi
{
public SteeringWheel SteeringWheel { get; set; }
public Pedal Accelerator { get; set; }
public Pedal Brake { get; set; }
}
public class AudiDriver
{
public void ParticipateInRace(Audi myAudi)
{
myAudi.Accelerator.Press();
myAudi.SteeringWheel.TurnLeft();
myAudi.SteeringWheel.TurnRight();
myAudi.Accelerator.Release();
myAudi.Brake.Press();
myAudi.Brake.Release();
}
}
这个驱动程序只知道如何驾驶奥迪。
但实际上,一名司机可以驾驶 任何 汽车(只要有方向盘和两个踏板)。
那么我们如何告诉编译器可以使用任何汽车呢?我们给它们一个共同点:接口。
public interface ICar
{
SteeringWheel SteeringWheel { get; }
Pedal Accelerator { get; }
Pedal Brake { get; }
}
public class BMW : ICar { /* same as before */ }
public class Audi : ICar { /* same as before */ }
public class Driver
{
public void ParticipateInRace(ICar anyCar)
{
anyCar.Accelerator.Press();
anyCar.SteeringWheel.TurnLeft();
anyCar.SteeringWheel.TurnRight();
anyCar.Accelerator.Release();
anyCar.Brake.Press();
anyCar.Brake.Release();
}
}
有一半的时间我会认为,如果你创建一个类并继承它,它不是也能做同样的事情吗?我错过了什么吗?
在某些情况下,继承是可行的。然而,继承通常是一种较差的解决方案,特别是当你进入更复杂的代码库或更高级的架构时。
不要担心,所有的开发者都曾经喜欢过继承,然后需要学习不要将继承作为万灵药。这是开发人员正常生命周期的一部分 :)
其中最大的原因之一是你不能从多个类派生,但可以实现多个接口。
假设我们有三种类型的运动可以进行
public class Runner
{
public void Run() { /* running logic */ }
}
public class Swimmer
{
public void Swim() { /* swimming logic */ }
}
public class Cyclist
{
public void Cycle() { /* cycling logic */ }
}
public class BasketBallPlayer : Runner
{
public void ThrowBall() { /* throwing logic */ }
// Running is already inherited from Runner
}
太好了,还没有问题。但现在,我们需要创建一个三项全能选手类,包括三项运动(跑步、游泳、骑自行车)。
public class Triathlete: Runner, Swimmer, Cyclist { ... }
这就是编译器崩溃的地方。它拒绝让你同时从多个基类继承。原因比本答案能够深入探讨的要复杂得多,如果你想了解更多,请搜索谷歌。
然而,如果我们使用了接口:
public interface IRunner
{
void Run();
}
public interface ISwimmer
{
void Swim();
}
public interface ICyclist
{
void Cycle();
}
那么编译器就会允许这样写:
public class Triathlete: IRunner, ISwimmer, ICyclist
{
public void Run() { /* running logic */ }
public void Swim() { /* swimming logic */ }
public void Cycle() { /* cycling logic */ }
}
这就是为什么接口通常比继承更好(除了其他原因外)。
有更多原因表明接口通常更好,但这是最重要的一个。如果你想要更多解释,请搜索一下谷歌,我不能在StackOverflow答案中详细阐述所有内容。
有一种情况是,您(整个算法的创建者)根本不知道预先实现的情况。
将它们转换为现实场景: FxCop + StyleCop都使用访问者模式来扫描代码。因此,在这个例子中,工具(FxCop)的创建者有一些基本代码,与一些UI/CLI耦合,并期望扫描结果中的某些特定属性,例如严重性/问题等。
虽然FxCop附带了默认规则,但作为最终客户,您也可以根据自己的喜好扩展这些规则。FxCop唯一能够做到这一点的方法就是依赖于多态接口/抽象类。
因此,FxCop工具期望一个规则实例,该实例检测某些内容并返回成功或失败。
但是,您的组织可能有一个只有您需要的自定义规则。比如说:我们所有的命名空间必须以myorg.mytool开头
这是一个必须使用抽象化的示例(不能仅仅在类中实现代码),因为Microsoft对您在组织中强制执行的自定义代码规则一无所知。
另一个例子是领域驱动设计中领域和基础设施代码分离的方式。
假设你有一个图书收藏应用程序,例如你可以获取一本书、获得某个作者的所有书籍等。
然后你会有一个域类型称为BookRepository,其中包含了所有的书籍持久化。这里有两个方面:1. 包含所有处理图书逻辑的域;2. 持久化代码(IO/数据库或其他)。
将这两者分开的原因是将域逻辑(业务逻辑)与持久化代码分开,使得它们不会相互纠缠。域代码不需要知道如何持久化图书,它只关心你可以对一本书做什么(按作者获取、购买、销售等)。
在这种情况下,界面是作为您在域代码中放置一个名为IBookRepository的接口并使用单元测试创建所需所有代码时出现的。此时,您不太关心书籍的存储方式 - 您只关心它们被存储了。然后,另一个团队或以后,您可以深入研究有关如何恢复图书的详细信息。在数据库、缓存或其他地方。持久性代码也可以在不触及域代码的情况下发展,这是持续发布原则的重要组成部分:尽可能小的更新。换句话说,它允许您运送基础设施更新而不影响业务代码。您的应用程序可能工作得很好,但您想更新数据库驱动程序。简而言之,对于每种情况,我能想到的最少的词汇:
例如,当您想要创建一个处理动物的应用程序时,您将使用抽象类:
您创建一个名为Animal
的抽象类,其中包含一些字段NoLegs
、HasTail
、NoEyes
。
您创建一个Dog
、Cat
和Panda
类,它们都继承自Animal
。
现在您最终拥有3个类,但是共享代码定义在另一个类中,因为它们都共享这些描述性特征。
现在来看接口:
在项目的服务内部,您创建了一个名为“StartRunning”的方法。 该方法的定义如下:
public void StartRunning(List<ICanRun> thingsThatRun)
{
thingsThatRun.forEach(t => t.StartRunning());
}
List<ICanRun> runableThings = new List<ICanRun>(){new Dog(), new Cat(), new Bike()};
StartRunning(runableThings);
我希望这个例子能够帮到你
IWaterInlet
-现在您有两个接口,一个用于水,一个用于电-使用基类无法做到这一点,因为C#不允许多重继承。 - James Thorpe