是否应该从编程语言中删除(非接口类型的)继承?

时间:2008-12-11 23:12:22

标签: language-agnostic inheritance programming-languages language-features

这是一个颇具争议的话题,在你说“不”之前,真的,真的需要吗?

我已经编程了大约10年,我不能说老实说我能回想起继承解决了一个无法用另一种方式解决的问题。另一方面,当我使用继承时,我可以回忆很多次,因为我觉得我必须这样做,或者因为我虽然我很聪明并最终为此付出了代价。

我无法真正看到任何情况,从实现的角度来看,无法使用聚合或其他技术而不是继承。

我唯一需要注意的是,我们仍然允许继承接口。

(更新)

让我们举一个例子说明为什么需要它而不是说“有时它只是需要它”。这根本没有用。你的证明在哪里?

(更新2代码示例)

这是经典的形状示例,更强大,更明确的IMO,没有继承。现实世界中的情况几乎从来都不是真的“是一种”的东西。几乎总是“以实施方式”更准确。

public interface IShape
{
    void Draw();
}

public class BasicShape : IShape
{
    public void Draw()
    {
        // All shapes in this system have a dot in the middle except squares.
        DrawDotInMiddle();
    }
}

public class Circle : IShape
{
    private BasicShape _basicShape;

    public void Draw()
    {
        // Draw the circle part
        DrawCircle();
        _basicShape.Draw();
    }
}

public class Square : IShape
{
    private BasicShape _basicShape;

    public void Draw()
    {
        // Draw the circle part
        DrawSquare();
    }
}

20 个答案:

答案 0 :(得分:20)

blogged about this不久前作为一个古怪的想法。

我不认为它应该删除,但我认为应该默认密封类阻止继承,如果不合适的话。它是一个功能强大的工具,但它就像一个链锯 - 你真的不想使用它,除非它是完美的工具。否则你可能会开始失去四肢。

这是潜在的语言功能,例如混合功能,可以让没有IMO的人更容易生活。

答案 1 :(得分:10)

在基类具有多个对每个派生类具有相同实现的方法的情况下,继承可能非常有用,以保存每个派生类不必实现样板代码。以.NET Stream类为例,它定义了以下方法:

public virtual int Read(byte[] buffer, int index, int count)
{
}

public int ReadByte()
{
    // note: this is only an approximation to the real implementation
    var buffer = new byte[1];
    if (this.Read(buffer, 0, 1) == 1)
    {
        return buffer[0];
    }

    return -1;
}

因为继承是可用的,所以基类可以为所有实现实现ReadByte方法,而不必担心它。在类上有许多其他类似于默认或固定实现的方法。所以在这种情况下,与一个界面相比,它是一个非常有价值的东西,你可以选择让每个人重新实现所有东西,或创建一个他们可以调用的StreamUtil类型类(yuk!)

为了澄清,继承所有我需要编写以创建DerivedStream类的内容类似于:

public class DerivedStream : Stream
{
    public override int Read(byte[] buffer, int index, int count)
    {
        // my read implementation
    }
}

然而,如果我们在StreamUtil中使用接口和方法的默认实现,我必须编写更多代码:

public class DerivedStream : IStream
{
    public int Read(byte[] buffer, int index, int count)
    {
        // my read implementation
    }

    public int ReadByte()
    {
        return StreamUtil.ReadByte(this);
    }
}

}

所以这不是一个更多的代码,而是通过类中的更多方法来增加它,这只是编译器可以处理的不必要的样板代码。为什么要使实施比必要的更痛苦?我不认为继承是最重要的,但是如果正确使用它会非常有用。

答案 2 :(得分:6)

我希望语言提供一些机制,以便更容易委托成员变量。例如,假设接口I有10个方法,C1类实现此接口。假设我想要实现类似C1的类C2,但是方法m1()被覆盖。如果不使用继承,我会按如下方式(在Java中)执行此操作:

public class C2 implements I {
    private I c1;

    public C2() {
       c1 = new C1();
    }

    public void m1() {
        // This is the method C2 is overriding.
    }

    public void m2() {
        c1.m2();
    }

    public void m3() {
        c1.m3();
    }

    ...

    public void m10() {
        c1.m10();
    }
}

换句话说,我必须明确地编写代码来将方法m2..m10的行为委托给成员变量m1。这有点痛苦。它也使代码混乱,因此很难在C2类中看到真正的逻辑。这也意味着无论何时向接口I添加新方法,我都必须向C1明确添加更多代码,以便将这些新方法委托给C1。

我希望语言允许我说:C1实现了我,但是如果C1缺少某些方法,我会自动委托给成员变量c1。这会将C1的大小减少到

public class C2 implements I(delegate to c1) {
    private I c1;

    public C2() {
       c1 = new C1();
    }

    public void m1() {
        // This is the method C2 is overriding.
    }
}

如果语言允许我们这样做,那么避免使用继承会容易得多。

这是我写的关于自动委派的blog article

答案 3 :(得分:6)

当然,你可以在没有对象和继承的情况下愉快地编写出色的程序;功能程序员一直这样做。但是,让我们不要仓促。任何对此主题感兴趣的人都应该查看Xavier Leroy关于classes vs modules in Objective Caml的邀请讲座的幻灯片。 Xavier做了一个很好的工作,在不同类型的软件演化的背景下,确定了继承做得好并且做得不好。

所有语言都是图灵完备的,因此当然不需要继承。但作为继承价值的论据,我提出了Smalltalk blue book,尤其是集合层次结构数字层次结构。令我印象深刻的是,熟练的专家可以在不影响现有系统的情况下添加全新的数字(或集合)。

我还会提醒提问者“继承人的杀手级应用程序”:GUI工具包。一个精心设计的工具包(如果你能找到)可以非常,非常容易地添加新的图形交互小部件。

说了这么多,我认为继承具有天生的弱点(你的程序逻辑被涂抹在一大堆类中)而且应该很少使用它,只能由熟练的专业人员使用。一个拥有计算机科学学士学位的人对继承知之甚少 - 应该允许这些人继承其他需要的课程,但绝不应该编写其他程序员继承的代码。这项工作应该留给那些真正知道自己在做什么的大师级程序员。他们应该不情愿地做到这一点!

对于使用完全不同的机制解决类似问题的有趣尝试,人们可能想要查看Haskell type classes

答案 4 :(得分:5)

在所有类上实现ISystemObject非常有趣,这样您就可以访问ToString()和GetHashcode()。

另外,祝IS300WebUIPage界面好运。

如果你不喜欢继承,我的建议是一起停止使用.NET。有太多的场景可以节省时间(参见DRY:不要重复自己)。

如果使用继承会破坏您的代码,那么您需要退后一步并重新考虑您的设计。

我更喜欢接口,但它们不是银弹。

答案 5 :(得分:5)

继承是可以使用的工具之一,当然可以被滥用,但我认为语言必须有更多的更改才能删除基于类的继承。

让我们抓住我的世界,主要是C#开发。

要让Microsoft取消基于类的继承,他们必须为处理接口提供更强大的支持。像聚合这样的东西,我需要添加大量的样板代码,只是为了将接口连接到内部对象。无论如何,这确实应该完成,但在这种情况下是必需的。

换句话说,以下代码:

public interface IPerson { ... }
public interface IEmployee : IPerson { ... }
public class Employee : IEmployee
{
    private Person _Person;
    ...

    public String FirstName
    {
        get { return _Person.FirstName; }
        set { _Person.FirstName = value; }
    }
}

这基本上要短得多,否则我会有很多这些属性只是为了让我的课程模仿一个人足够好,就像这样:

public class Employee : IEmployee
{
    private Person _Person implements IPerson;
    ...
}

这可以自动创建必要的代码,而不是我必须编写它。如果我将对象转换为IPerson,只返回内部引用就没有用了。

因此,在基于类的继承可以脱离桌面之前,必须更好地支持这些事情。

此外,您还会删除可见性等内容。一个界面真的只有两个可见性设置:那里,而不是那里。在某些情况下,您或者我认为会被迫公开更多内部数据,以便其他人可以更轻松地使用您的课程。

对于基于类的继承,您通常可以公开一些后代可以使用的访问点,但外部代码不能,并且您通常只需要删除这些访问点,或者让它们对所有人开放。不确定我是否喜欢其他选择。

我最大的问题是具体删除此类功能的重点是什么,即使计划是作为一个例子,构建D#,一种新的语言,如C#,但没有基于类的继承。换句话说,即使你打算建立一种全新的语言,我仍然不能完全确定最终的目标是什么。

目标是移除可能被滥用的东西,如果不是在正确的手中?如果是这样,我有一个一英里长的列表,用于各种编程语言,我会真的喜欢先查看地址。

在该列表的顶部:Delphi中的 with 关键字。这个关键词不仅仅是用脚射击自己,就像编译器购买霰弹枪一样,来到你家并瞄准你。

我个人喜欢基于类的继承。当然,你可以把自己写进一个角落。但我们都可以这样做。删除基于类的继承,我只会找到一种用脚射击自己的新方法。

现在我把霰弹枪放在哪里......

答案 6 :(得分:4)

对于生产代码,我几乎从不使用继承。我使用接口来处理所有事情(这有助于测试并提高可读性,即你可以只看一下界面来阅读公共方法,看看因为命名良好的方法和类名而发生了什么)。几乎是我唯一一次使用继承是因为第三方库需要它。使用接口,我会得到相同的效果,但我会使用'委托'来模仿继承。

对我而言,这不仅更具可读性,而且更易于测试,并且使重构变得更加轻松。

我唯一能想到的是,我会在测试中使用继承来创建我自己的特定TestCases,用于区分我系统中的测试类型。

所以我可能不会摆脱它,但出于上述原因,我选择尽可能不使用它。

答案 7 :(得分:3)

需要的?不可以。例如,您可以用C语言编写任何程序,它没有任何类型的继承或对象。你可以用汇编语言编写它,虽然它不那么便携。你可以在图灵机上写它并模拟它。有人设计了一种只有一条指令的计算机语言(如果不是零的话,就像减法和分支一样),你可以用它来编写你的程序。

因此,如果您要询问是否需要某个语言功能(如继承,对象,递归或函数),答案是否定的。 (有一些例外 - 您必须能够循环并有条件地执行操作,尽管这些不需要作为语言中的显式概念支持。)

因此,我觉得这类问题毫无用处。

诸如“我们何时应该使用继承”或“我们什么时候不应该”的问题更有用。

答案 8 :(得分:3)

我想我必须扮演魔鬼的拥护者。如果我们没有继承,那么我们将无法继承使用模板方法模式的抽象类。有很多例子可以在.NET和Java等框架中使用它。 Java中的线程就是这样一个例子:

// Alternative 1:
public class MyThread extends Thread {

    // Abstract method to implement from Thread
    // aka. "template method" (GoF design pattern)
    public void run() {
        // ...
    }
}

// Usage:
MyThread t = new MyThread();
t.start();

当我必须使用它时,替代方案是 verbose 。视觉混乱的复杂性上升。这是因为您需要在实际使用之前创建线程。

// Alternative 2:
public class MyThread implements Runnable {
    // Method to implement from Runnable:
    public void run() {
        // ...
    }
}

// Usage:
MyThread m = new MyThread();
Thread t = new Thread(m);
t.start();
// …or if you have a curious perversion towards one-liners
Thread t = new Thread(new MyThread());
t.start();

让我的魔鬼主张脱帽我想你可以争辩说,第二个实现中的收益是依赖注入或关注分离,这有助于设计可测试的类。根据您对接口的定义(我已经听说过至少三个),抽象类可以被视为接口。

答案 9 :(得分:2)

没有。仅仅因为它并不经常需要,并不意味着它永远不需要。与工具包中的任何其他工具一样,它可以(并且已经和将要)被滥用。但是,这并不意味着它应该从不使用。事实上,在某些语言(C ++)中,语言级别没有“接口”这样的东西,所以没有重大改变,你就无法禁止它。

答案 10 :(得分:2)

很多时候我发现自己在界面上选择基类只是因为我有一些标准功能。在C#中,我现在可以使用扩展方法来实现这一点,但在几种情况下它仍然没有达到同样的效果。

答案 11 :(得分:2)

没有。有时你需要继承。而对于那些你不这样做的时候 - 不要使用它。您可以随时“只”使用接口(使用具有它们的语言)和没有数据的ADP,就像那些没有数据的语言中的接口一样。但我认为没有理由去除有时必要的功能只是因为你觉得并不总是需要它。

答案 12 :(得分:1)

不,它不是需要,但这并不意味着它不能提供整体效益,我认为这比担心它是否绝对必要更重要。

最后,几乎所有现代软件语言结构都构成语法糖 - 如果我们真的需要,我们都可以编写汇编代码(或使用穿孔卡,或使用真空管)。

在那些我真正想要表达“是一种”关系的时候,我发现继承非常有用。继承似乎是表达这种意图的最明确的手段。如果我使用委托重新使用所有实现,我就会失去那种表现力。

这是否允许滥用?当然可以。我经常看到一些问题,询问开发人员如何从类继承但隐藏方法,因为该方法不应该存在于子类中。那个人显然错过了继承点,应该指向代表团。

我不使用继承,因为它是必需的,我使用它,因为它有时是最好的工具。

答案 13 :(得分:1)

请注意,继承意味着不再可能通过依赖注入提供基类功能,以便单独测试派生类与其父级的隔离。

因此,如果你对依赖注入非常认真(我不是,但我确实想知道我是否应该这样做),你无论如何也无法充分利用继承。

答案 14 :(得分:1)

这是一个很好的主题观点:

IS-STRICTLY-EQUIVALENT-TO-A by Reg Braithwaite

答案 15 :(得分:1)

真的需要继承吗?取决于你的意思“真的”。理论上你可以回到打卡或轻击切换开关,但这是一种开发软件的可怕方法。

在过程语言中,是的,类继承是一个明确的福音。它为您提供了在特定情况下优雅地组织代码的方法。不应过度使用,因为任何其他功能都不应过度使用。

例如,以此线程中的di​​giarnie为例。他/她几乎使用接口,这与使用大量继承一样糟糕(可能更糟)。

他的一些观点:

  

这有助于测试并提高可读性

它没有做任何事情。你永远不会真正测试一个接口,你总是测试一个对象,即一个类的实例化。而且必须查看完全不同的代码才能帮助您理解类的结构?我不这么认为。

同样深入了解继承层次结构。理想情况下,您只想在一个地方看。

  

使用接口,我会得到相同的效果,但我会通过使用模仿继承   '委托'。

委托是一个非常好的主意,应该经常使用而不是继承(例如,策略模式就是完全按照这个做法)。但接口与委托无关,因为您无法在接口中指定任何行为。

  

也使重构变得更容易。

对接口的早期承诺通常会使重构变得更难,而不是更容易,因为有更多的地方需要改变。早期过度使用继承比过度使用接口更好(更好,更糟糕),因为如果被修改的类没有实现任何接口,则删除委托类会更容易。而且经常来自这些代表而不是获得有用的界面。

因此过度使用继承是一件坏事。过度使用接口是一件坏事。理想情况下,类不会继承任何东西(除了可能是“对象”或语言等价物),也不会实现任何接口。但这并不意味着应从语言中删除这两种功能。

答案 16 :(得分:1)

我最初的想法是,你疯了。但经过一段时间的考虑,我有点同意你的看法。我并不是说完全删除类继承(例如,部分实现的抽象类可能很有用),但我经常继承(双关语)写得很糟糕的OO代码,它具有多级别的继承,除了膨胀之外什么也没有增加代码。

答案 17 :(得分:1)

如果有一个几乎完全符合你想要的框架类,但是它的接口的某个特定函数会抛出一个NotSupported异常,或者由于某些其他原因你只想覆盖一个方法来执行特定于你的实现的东西,那就太多了更容易编写子类并覆盖那个方法,而不是编写一个全新的类,并为类中的其他27个方法编写传递。

同样,Java也是如此,例如,每个对象都从Object继承,因此自动具有equals,hashcode等的实现。我不必重新实现它们,并且它“只是工作”当我想要将对象用作哈希表中的键。我没有必要为Hashtable.hashcode(Object o)方法写一个默认直通,坦率地说它似乎正在从对象方向移动

答案 18 :(得分:1)

我认为有时通过继承实现代码重用的更好机制是traits。请查看此link (pdf)以获得有关此问题的深入讨论,包括特征和混合素之间的区别,以及为什么特性受到青睐。

有一些研究将特征引入C# (pdf)

Perl的特征是Moose::Roles。 Scala特征与mixins类似Ruby

答案 19 :(得分:0)

问题是,“是否应该从编程语言中删除(非接口类型的)继承?”

我说,“不”,因为它会打破许多现有代码的地狱。

除此之外,您应该使用继承,而不是继承接口吗?我主要是一个C ++程序员,我遵循接口的多重继承的严格对象模型,然后是一系列单继承的。具体的类是组件的“秘密”,它是朋友,所以那里发生的是无人机业务。

为了帮助实现接口,我使用模板mixins。这允许界面设计者提供代码片段以帮助实现常见场景的界面。作为一个组件开发人员,我觉得我可以去混合购买以获得可重复使用的位,而不会受到界面设计者认为我应该构建我的类的困扰。

话虽如此,mixin范例对于C ++来说几乎是独一无二的。如果没有这个,我希望继承对实用程序员非常有吸引力。