63

我知道这个问题之前已经讨论过,但似乎总是假设继承至少有时比组合更可取。我想挑战这个假设,希望获得一些理解。

我的问题是这样的:既然你可以用对象组合来完成任何你可以用经典继承来完成的事情,而且因为经典继承经常被滥用[1] ,而且由于对象组合让你可以灵活地更改委托对象运行时,为什么你使用经典传承?

我可以理解为什么您会建议在 Java 和 C++ 等不提供方便的委派语法的语言中使用继承。在这些语言中,只要不明显不正确,您就可以通过使用继承来节省大量输入。但是其他语言,如 Objective C 和 Ruby,提供了经典的继承非常方便的委派语法。据我所知,Go 编程语言是唯一一种认为经典继承比它的价值更麻烦并且只支持代码重用的委托的语言。

陈述我的问题的另一种方式是:即使您知道经典继承对于实现某个模型并没有错误,是否有足够的理由使用它而不是组合?

[1] 许多人使用经典继承来实现多态性,而不是让他们的类实现接口。继承的目的是代码重用,而不是多态。此外,有些人使用继承来模拟他们对“is-a”关系的直观理解,这通常是有问题的。

更新

当我谈论继承时,我只是想澄清一下我的意思:

我说的是类继承自部分或完全实现的基类的那种继承。我不是在谈论从一个纯粹的抽象基类继承,这相当于实现一个接口,我郑重声明并不反对。

更新 2

我知道继承是在 C++ 中实现多态性的唯一方法。在这种情况下,很明显为什么你必须使用它。所以我的问题仅限于提供不同方法来实现多态性(分别为接口和鸭子类型)的 Java 或 Ruby 等语言。

4

13 回答 13

54

[注意:这个问题最初被标记为与语言无关。基于此,这个答案被写成与语言无关,因此它讨论了继承,因为它在广泛的语言中使用,例如 Smalltalk、C++ 和 Object Pascal。此后,它被重新标记为专门针对 Java。Java 在将 aclass和 an定义interface为两个完全独立的事物方面有所不同。从 Java 特定的角度来看,继承的目的是代码重用,而不是多态性的想法是合理的,但从与语言无关的角度来看,显然是错误的。如果您只关心 Java,这可能不是最佳答案。]

继承的目的是代码重用,而不是多态。

这是你的根本错误。几乎完全相反。(公共)继承的主要目的是为所讨论的类之间的关系建模。多态性是其中很大一部分。

如果使用得当,继承与重用现有代码无关。相反,它是关于被现有代码使用。也就是说,如果您有可以与现有基类一起使用的现有代码,那么当您从该现有基类派生一个新类时,其他代码现在也可以自动与您的新派生类一起使用。

可以使用继承来重用代码,但是当/如果你这样做,它通常应该是私有继承而不是公共继承。如果您使用的语言很好地支持委托,那么您很有可能很少有理由使用私有继承。OTOH,私有继承确实支持委托(通常)不支持的一些事情。特别是,即使在这种情况下多态性绝对是次要问题,它仍然可能是一个问题——即,使用私有继承,您可以从几乎是您想要的基类开始,并且(假设它允许)覆盖不太正确的部分。

使用委托,您唯一真正的选择是完全按原样使用现有类。如果它不能完全满足您的要求,您唯一真正的选择是完全忽略该功能,并从头开始重新实现它。在某些情况下,这并没有什么损失,但在其他情况下却是相当可观的。如果基类的其他部分使用多态函数,私有继承允许您覆盖多态函数,而其他部分将使用您覆盖的函数。使用委托,您无法轻松插入新功能,因此现有基类的其他部分将使用您已覆盖的功能。

于 2010-07-28T15:23:59.063 回答
18

使用继承的主要原因不是作为一种组合形式——它是为了获得多态行为。如果您不需要多态性,您可能不应该使用继承,至少在 C++ 中是这样。

于 2010-07-28T09:52:32.470 回答
16

如果您将尚未明确覆盖的所有内容委托给实现相同接口的其他对象(“基础”对象),那么您基本上已经在组合之上实现了 Greenspunned 继承,但(在大多数语言中)更加冗长和样板。使用组合而不是继承的目的是让您只能委托您想要委托的行为。

如果您希望对象使用基类的所有行为,除非显式覆盖,那么继承是最简单、最简洁、最直接的表达方式。

于 2010-07-28T13:38:40.253 回答
7

每个人都知道多态性是继承的一大优势。我在继承中发现的另一个好处是有助于创建现实世界的复制品。例如,在工资单系统中,如果我们用超类 Employee 继承所有这些类,我们会处理经理开发人员、办公室男孩等。它使我们的程序在现实世界中更容易理解,所有这些类基本上都是员工。还有一件事,类不仅包含方法,还包含属性。因此,如果我们在 Employee 类中包含对员工通用的属性,例如社会安全号码年龄等,它将提供更好的代码重用和概念清晰度,当然还有多态性。但是,在使用继承时,我们应该牢记的是基本设计原则“

于 2012-11-25T15:44:07.180 回答
6

如果出现以下情况,则优先选择继承:

  1. 您需要公开您扩展的类的整个 API(使用委托,您将需要编写大量委托方法),并且您的语言没有提供一种简单的方式来表示“将所有未知方法委托给”。
  2. 您需要访问没有“朋友”概念的语言的受保护字段/方法
  3. 如果您的语言允许多重继承,则委派的优势会有所降低
  4. 如果您的语言允许在运行时从类甚至实例动态继承,您通常根本不需要委托。如果您可以同时控制公开哪些方法(以及公开它们的方式),则根本不需要它。

我的结论:委托是编程语言中错误的一种解决方法。

于 2010-07-28T09:56:16.233 回答
4

在使用继承之前我总是三思而后行,因为它很快就会变得棘手。话虽如此,在很多情况下它只是生成最优雅的代码。

于 2010-07-28T13:41:31.857 回答
3

接口只定义对象可以做什么而不是如何做。所以简单来说,接口就是合约。所有实现接口的对象都必须定义自己的合约实现。在实际世界中,这给了你separation of concern. 想象一下自己编写的应用程序需要处理您事先不知道的各种对象,但您仍然需要处理它们,您只知道这些对象应该做什么不同的事情。因此,您将定义一个接口并在合同中提及所有操作。现在您将针对该接口编写应用程序。以后任何想要利用您的代码或应用程序的人都必须在对象上实现接口以使其与您的系统一起使用。您的界面将强制他们的对象定义合同中定义的每个操作应该如何完成。这样,任何人都可以编写实现您的接口的对象,以使它们完美地适应您的系统,而您所知道的只是需要做什么,而需要定义如何完成的是对象。

在实际开发中,这种做法通常被称为 Programming to Interface and not to Implementation.

接口只是合同或签名,它们对实现一无所知。

针对接口进行编码意味着,客户端代码始终持有一个由工厂提供的接口对象。工厂返回的任何实例都是任何工厂候选类必须实现的接口类型。这样客户端程序就不用担心实现了,接口签名决定了所有操作可以做什么。这可以用来改变程序在运行时的行为。从维护的角度来看,它还可以帮助您编写更好的程序。

这是一个基本的例子。

public enum Language
{
    English, German, Spanish
}

public class SpeakerFactory
{
    public static ISpeaker CreateSpeaker(Language language)
    {
        switch (language)
        {
            case Language.English:
                return new EnglishSpeaker();
            case Language.German:
                return new GermanSpeaker();
            case Language.Spanish:
                return new SpanishSpeaker();
            default:
                throw new ApplicationException("No speaker can speak such language");
        }
    }
}

[STAThread]
static void Main()
{
    //This is your client code.
    ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English);
    speaker.Speak();
    Console.ReadLine();
}

public interface ISpeaker
{
    void Speak();
}

public class EnglishSpeaker : ISpeaker
{
    public EnglishSpeaker() { }

    #region ISpeaker Members

    public void Speak()
    {
        Console.WriteLine("I speak English.");
    }

    #endregion
}

public class GermanSpeaker : ISpeaker
{
    public GermanSpeaker() { }

    #region ISpeaker Members

    public void Speak()
    {
        Console.WriteLine("I speak German.");
    }

    #endregion
}

public class SpanishSpeaker : ISpeaker
{
    public SpanishSpeaker() { }

    #region ISpeaker Members

    public void Speak()
    {
        Console.WriteLine("I speak Spanish.");
    }

    #endregion
}

替代文字 http://ruchitsurati.net/myfiles/interface.png

于 2010-07-28T10:22:22.227 回答
2

模板方法模式呢?假设您有一个基类,其中包含大量可定制策略的点,至少由于以下原因之一,策略模式没有意义:

  1. 可定制的策略需要了解基类,只能与基类一起使用,在任何其他上下文中都没有意义。使用策略代替是可行的,但 PITA 因为基类和策略类都需要相互引用。

  2. 这些政策是相互耦合的,自由混合搭配它们是没有意义的。它们仅在所有可能组合的非常有限的子集中才有意义。

于 2010-07-28T13:33:55.137 回答
0

我看到使用继承的最有用的方法之一是在 GUI 对象中。

于 2010-07-28T14:18:46.163 回答
0

完全不是面向对象的东西,但是组合通常意味着额外的缓存未命中。它确实取决于,但让数据更接近是一个加号。

一般来说,我拒绝参加一些宗教斗争,用你自己的判断和风格是你能得到的最好的。

于 2011-02-09T10:15:48.723 回答
0

经典继承的主要用途是,如果您有许多相关的类,它们对于操作实例变量/属性的方法具有相同的逻辑。

实际上有3种方法可以处理:

  1. 遗产。
  2. 复制代码(代码气味“重复代码”)。
  3. 将逻辑移到另一个类(代码闻起来像“懒惰类”、“中间人”、“消息链”和/或“不恰当的亲密关系”)。

现在,可能会滥用继承。例如,Java 有类InputStreamOutputStream. 这些子类用于读/写文件、套接字、数组、字符串,还有一些用于包装其他输入/输出流。根据他们所做的,这些应该是接口而不是类。

于 2010-07-28T14:01:39.427 回答
0

你写了:

[1] 许多人使用经典继承来实现多态性,而不是让他们的类实现接口。继承的目的是代码重用,而不是多态。此外,有些人使用继承来模拟他们对“is-a”关系的直观理解,这通常是有问题的。

在大多数语言中,“实现一个接口”和“从另一个类派生一个类”之间的界线非常细。事实上,在像 C++ 这样的语言中,如果你从类 A 派生类 B,而 A 是一个只包含纯虚方法的类,那么你就是在实现一个接口。

继承是关于接口重用,而不是实现重用。正如您在上面所写的,这与代码重用无关。

正如您正确指出的那样,继承旨在为 IS-A 关系建模(许多人犯错的事实与继承本身无关)。你也可以说“像一个人一样”。然而,仅仅因为某事物与其他事物具有 IS-A 关系并不意味着它使用相同(或什至相似)的代码来实现这种关系。

比较这个实现不同输出数据方式的 C++ 示例;两个类使用(公共)继承,以便可以多态地访问它们:

struct Output {
  virtual bool readyToWrite() const = 0;
  virtual void write(const char *data, size_t len) = 0;
};

struct NetworkOutput : public Output {
  NetworkOutput(const char *host, unsigned short port);

  bool readyToWrite();
  void write(const char *data, size_t len);
};

struct FileOutput : public Output {
  FileOutput(const char *fileName);

  bool readyToWrite();
  void write(const char *data, size_t len);
};

现在想象一下,如果这是 Java。“输出”不是结构,而是“接口”。它可能被称为“可写”。你会说“实现可写”而不是“公共输出”。就设计而言,有什么区别?

没有任何。

于 2010-07-28T10:14:22.040 回答
0

当你问:

即使您知道经典继承对于实现某个模型并没有错,那是否足以使用它而不是组合?

答案是不。如果模型不正确(使用继承),那么无论如何使用都是错误的。

以下是我见过的一些继承问题:

  1. 总是必须测试派生类指针的运行时类型,以查看它们是否可以向上(或向下)。
  2. 这种“测试”可以通过多种方式实现。您可能有某种返回类标识符的虚拟方法。或者您可能必须实现 RTTI(运行时类型识别)(至少在 c/c++ 中),这可能会给您带来性能损失。
  3. 未能“强制转换”的类类型可能会出现问题。
  4. 有很多方法可以在继承树上上下转换你的类类型。
于 2010-07-28T10:25:24.310 回答