理解接口与类的区别

3

你好,我知道这是一个经常出现的问题,但是在阅读了以下帖子之后(什么是接口和类的区别?为什么我应该使用接口而不是在类中直接实现方法?),我仍然很难理解为什么需要使用接口。对于这个基础问题感到抱歉,但是虽然我理解接口和类之间的关系是一种合同,但我似乎无法看到它的实用性。我知道它可以帮助您轻松创建对象,但我感觉我错过了什么。

我已经在这里和整个互联网上阅读了很多关于如何使用接口的帖子,但是有一半的时间我会想,如果你创建一个类并继承它,它不会做同样的事情吗?我在这里错过了什么?


在另一个问题的答案中看到了您的评论:“如果需要,您是否可以不创建PowerSocket的基类并使所有其他内容继承它?”水壶和洗衣机都需要水和电,它们可能还会实现IWaterInlet-现在您有两个接口,一个用于水,一个用于电-使用基类无法做到这一点,因为C#不允许多重继承。 - James Thorpe
这在多重继承方面是说得通的。然而,我无法想象接口仅仅因为多重继承而被使用。肯定还有其他我没有考虑到的原因。 - altaaf.hussein
1
你读过这篇文章了吗? - Sweeper
简而言之,我想提醒大家的是,基类定义了子类的基本行为 - 而接口仅定义了针对特定受众的操作。因此,接口是为单一类型的用户设计的,而基类是为类的内部工作设计的。这也符合您可以继承多个接口,但只有一个基类的行为。 - C.Evenhuis
我总是以你家的电源插座为例子。 你不需要为家里每个电器准备一个插座,只需将它们插入同一个插座即可让它们全部工作。 - Hypenate
你并没有错过什么,多重继承确实是接口和抽象基类之间唯一具有显著功能差异的东西,因为它没有任何默认实现。 - Craig
3个回答

5

为什么需要接口?

你开车吗?如果不是,我假设你知道驾驶汽车的一般情况(方向盘,油门,刹车)。下面的回答假定你会开车,且你的车和我的不同。

你曾经开过我的车吗?没有。但是,如果有机会,你能否在无需学习如何开我的车的情况下开我的车?可以。
这也适用于我。我从未开过你的车,但我也能够驾驶它而无需学会如何开车。

为什么呢?因为所有的汽车都有共同的接口。方向盘,右手油门,中间的刹车。没有两辆汽车是完全相同的,但它们都是以让驾驶员和任何汽车之间的互动完全相同的方式构建的。

将此与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答案中详细阐述所有内容。


0

有一种情况是,您(整个算法的创建者)根本不知道预先实现的情况。

将它们转换为现实场景: FxCop + StyleCop都使用访问者模式来扫描代码。因此,在这个例子中,工具(FxCop)的创建者有一些基本代码,与一些UI/CLI耦合,并期望扫描结果中的某些特定属性,例如严重性/问题等。

虽然FxCop附带了默认规则,但作为最终客户,您也可以根据自己的喜好扩展这些规则。FxCop唯一能够做到这一点的方法就是依赖于多态接口/抽象类。

因此,FxCop工具期望一个规则实例,该实例检测某些内容并返回成功或失败。

但是,您的组织可能有一个只有您需要的自定义规则。比如说:我们所有的命名空间必须以myorg.mytool开头

这是一个必须使用抽象化的示例(不能仅仅在类中实现代码),因为Microsoft对您在组织中强制执行的自定义代码规则一无所知。

另一个例子是领域驱动设计中领域和基础设施代码分离的方式。

假设你有一个图书收藏应用程序,例如你可以获取一本书、获得某个作者的所有书籍等。

然后你会有一个域类型称为BookRepository,其中包含了所有的书籍持久化。这里有两个方面:1. 包含所有处理图书逻辑的域;2. 持久化代码(IO/数据库或其他)。

将这两者分开的原因是将域逻辑(业务逻辑)与持久化代码分开,使得它们不会相互纠缠。域代码不需要知道如何持久化图书,它只关心你可以对一本书做什么(按作者获取、购买、销售等)。

在这种情况下,界面是作为您在域代码中放置一个名为IBookRepository的接口并使用单元测试创建所需所有代码时出现的。此时,您不太关心书籍的存储方式 - 您只关心它们被存储了。然后,另一个团队或以后,您可以深入研究有关如何恢复图书的详细信息。在数据库、缓存或其他地方。持久性代码也可以在不触及域代码的情况下发展,这是持续发布原则的重要组成部分:尽可能小的更新。换句话说,它允许您运送基础设施更新而不影响业务代码。您的应用程序可能工作得很好,但您想更新数据库驱动程序。
抽象类介于接口和类之间,但应类似于接口使用/它们在使用上与接口更接近而不是类。
最后,使用接口的另一个原因是,接口可以被认为是一种方面,您可以将多个接口(方面)应用于单个类(多重继承),几乎没有摩擦,而将其放置在类中会迫使您进行单一继承,这可能导致庞大且过于复杂的继承层次结构。
希望对您有所帮助

0

简而言之,对于每种情况,我能想到的最少的词汇:

  • 当您有不同实体共享相同行为时,应使用抽象类。
  • 当您希望不同实体能够使用相同代码时,应使用接口。

例如,当您想要创建一个处理动物的应用程序时,您将使用抽象类:

您创建一个名为Animal的抽象类,其中包含一些字段NoLegsHasTailNoEyes。 您创建一个DogCatPanda类,它们都继承自Animal。 现在您最终拥有3个类,但是共享代码定义在另一个类中,因为它们都共享这些描述性特征。

现在来看接口:

在项目的服务内部,您创建了一个名为“StartRunning”的方法。 该方法的定义如下:

public void StartRunning(List<ICanRun> thingsThatRun)
{
    thingsThatRun.forEach(t => t.StartRunning());
}

现在你回到你的动物抽象类中,声明它应该实现ICanRun接口,编译器会强制你去定义Dog、Cat和Panda类上的方法。现在的好处是:
  1. 你只需要一次定义不同对象共有的属性(例如描述动物时的眼睛数量)。
  2. 每种动物的奔跑方式都不同。你的狗可以“径直向前奔跑”,而你的猫可能会“寻找最近的墙壁并爬上去”。熊猫可能会“抛出异常”,因为熊猫不会奔跑。
接口的真正魔力在于: 因为你的StartRunning方法不考虑类,而是考虑传递的对象是否符合ICanRun接口,所以你可以有另一个类,比如自行车(Bike),也实现ICanRun接口并定义了StartRunning方法。 然后你可以创建一个包含狗、猫和自行车的列表,并将这三个不相关的类传递给相同的列表,然后将该列表传递给StartRunning方法,它们都会“开始奔跑”。
List<ICanRun> runableThings = new List<ICanRun>(){new Dog(), new Cat(), new Bike()};
StartRunning(runableThings);

我希望这个例子能够帮到你


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接