【问题标题】:Understanding Interfaces V Classes理解接口 V 类
【发布时间】:2020-02-13 01:14:39
【问题描述】:

嗨,我知道这是一个老生常谈的问题,但在阅读了以下帖子 What is the difference between an interface and a class, and why I should use an interface when I can implement the methods directly in the class? 后,我很难理解为什么真的需要使用接口。对于这里的基本问题很抱歉,但是当我将理论作为接口和类之间的契约时,我似乎看不出这是多么有用。我知道它可以帮助您轻松创建对象,但我觉得我错过了一些东西。

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

【问题讨论】:

  • 看到your comment on one of the answers 另一个问题:“你能不能不为 PowerSocket 创建一个基类,如果需要,所有其他东西都继承它” 水壶和洗衣机机器既需要水也需要电力,它们也可能实现IWaterInlet - 现在你有两个接口,一个用于水,一个用于电力 - 你不能用基类来做到这一点,因为 C# 不允许多重继承。
  • 足够公平,对多重继承有意义。但是我无法想象接口的使用纯粹是由于多重继承。我一定还缺少其他东西。
  • 你读过this吗?
  • 简而言之,我想记住,基类定义了子类的 基本行为——其中接口只定义了针对特定的操作i> 观众。因此,接口的设计考虑了单一类型的用户,而基类的设计考虑了类的内部工作。这也符合您可以继承多个接口但只有一个基类的行为。
  • 我总是以你家的电源插座为例。您不希望家中的每一个电子设备都有一个插座,您将其插入同一个插座,它们都可以工作。

标签: c# .net vb.net oop


【解决方案1】:

为什么是接口?

你会开车吗?如果不是,我假设您知道驾驶汽车通常需要什么(方向盘、油门、刹车)。其余答案假设您驾驶汽车并且拥有与我不同品牌的汽车。

你开过我的车吗?不会。但是如果获得访问权限,您是否能够在无需学习如何驾驶我的汽车的情况下驾驶我的汽车?是的。
这同样适用于我。我从来没有开过你的车,但我可以驾驶它而无需学习如何驾驶它。

这是为什么呢? 因为所有汽车共享相同的界面。方向盘,油门在右边,刹车在中间。没有两辆汽车是完全相同的,但它们的构建方式是驾驶员与任何汽车之间的交互是完全相同的。

将此与 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();
    }
}

我们现在有一个更通用的Driver,他能够驾驶任何有方向盘和两个踏板的汽车。


为什么不继承?

如果你创建一个类并继承它,我有一半的时间不会做同样的事情吗?我在这里错过了什么?

在某些情况下,继承会起作用。但是,继承通常是一种较差的解决方案,尤其是当您进入更复杂的代码库或更高级的架构时。

别担心,所有开发人员都曾经喜欢继承,然后需要学会不使用继承作为万灵药。这是开发人员正常生命周期的一部分:)

最大的原因之一是不能从多个类派生,但可以实现多个接口

假设我们可以进行三种类型的运动

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 答案。

【讨论】:

    【解决方案2】:

    从本质上讲,我对每种情况都用尽可能少的词:

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

    例如,你会在以下情况下使用抽象:

    您想创建一个处理动物的应用。 您创建一个抽象类Animal,其中包含一些字段NoLegs,HasTail,NoEyes。 您创建了一个 Dog 、一个 Cat 和一个 Panda 类,它们都继承自 Animal。 现在您最终拥有了 3 个类,但共享代码在其他 1 个类中定义,因为它们都具有这些描述性特征。

    现在界面: 在项目的服务中,您创建一个名为“StartRunning”的方法。 方法的定义是:

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

    现在你回到你的动物抽象类并声明它应该实现ICanRun接口,编译器将强制你去定义DogCatPanda类的方法。现在最重要的是:

    1. 您只定义了一次在不同对象中通用的属性。 (眼睛的数量是描述动物时常见的特征)
    2. 每只动物的奔跑方式都不同。你的狗跑法可以“直接往前跑”,而你的猫跑法可以“寻找最近的墙并爬上去”。熊猫可能会throw 导致熊猫不跑。

    界面上真正的魔力: 因为您的 StartRunning 方法不考虑类,而是将对象传递给ICanRun 接口,所以您可以拥有另一个类,例如Bike,它也实现ICanRun 并定义方法StartRunning。 然后,您可以创建一个包含狗、猫和自行车的列表,并将这 3 个不相关的类放到同一个列表中,并将该列表传递给 StartRunning,它们都会“开始跑步”。

    List<ICanRun> runableThings = new List<ICanRun>(){new Dog(), new Cat(), new Bike()};
    StartRunning(runableThings);
    

    希望这个例子对你有帮助

    【讨论】:

      【解决方案3】:

      您(整体算法的创建者)根本不知道实现的情况。

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

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

      因此,FxCop 工具需要一个规则实例,它可以检测到某些东西并报告成功或失败。

      但您的组织可能有一个只有您需要的自定义规则。假设它是这样的:我们所有的命名空间都必须以 myorg.mytool 开头

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

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

      假设您有一个图书收藏应用。您可以在其中获得一本书,作者的所有书籍等。

      然后,您将有一个域类型调用,例如 BookRepository,您的所有书籍都将保存在其中。这有两个方面:1. 放置书籍的所有处理逻辑的域和 2. 持久性代码(IO/数据库或其他)。

      将这两者分开的原因是因为域逻辑(业务逻辑)不会与持久性代码纠缠在一起。域代码不想知道一本书是如何被持久化的。它只关心您可以对一本书做什么(按作者获取、购买、出售等)。

      在这种情况下,当您在域代码中放置一个名为 IBookRepository 之类的接口时,接口就会出现,然后您继续使用单元测试创​​建您需要的所有代码。在这一点上,您并不关心书籍是如何存储的——您只关心它们是如何存储的。然后另一个团队或以后,您可以深入了解有关该书如何恢复的详细信息。在数据库中,在缓存中或其他东西中。持久性代码也可以在不触及作为持续发布原则的组成部分的域代码的情况下发展:尽可能小的更新。换句话说,它允许您在触及业务代码的情况下发布基础架构更新。可能是您的应用程序运行良好,但您不想更新数据库驱动程序。

      抽象类是介于接口和类之间的东西,但应该像接口一样使用/它们在使用上更接近于接口而不是类。

      ** 最后,使用接口的另一个原因是接口可以被视为一个方面,您可以将多个接口(方面)应用于单个类(多重继承)而几乎没有摩擦,而将其放在类中会迫使您进行单一继承,这可能会导致大且过于复杂的继承层次结构。

      希望对你有所帮助

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2023-03-19
        • 1970-01-01
        • 1970-01-01
        • 2020-06-08
        • 1970-01-01
        • 1970-01-01
        • 2014-11-21
        相关资源
        最近更新 更多