为什么要使用继承?

时间:2010-07-28 09:48:45

标签: java oop language-agnostic inheritance composition

我知道问题一直是discussed before,但似乎总是假设继承至少有时比组合更可取。我想挑战这个假设,希望获得一些理解。

我的问题是:开始,您可以使用经典继承来完成任何具有对象组合的内容,并且因为经典继承经常被滥用[1] 并且由于对象组合使您可以灵活地更改委托对象运行时,为什么使用经典继承?

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

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

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

更新

我只想在谈到继承时澄清我的意思:

我在谈论the kind of inheritance whereby a class inherits from a partially or fully implemented base class。我谈论从纯粹抽象的基类继承,这与实现接口相同,我记录的并不反对。

更新2

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

13 个答案:

答案 0 :(得分:46)

  

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

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

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

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

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

答案 1 :(得分:16)

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

答案 2 :(得分:10)

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

如果希望对象使用基类的所有行为,除非显式重写,那么继承是表达它的最简单,最简单,最简单的方法。

答案 3 :(得分:7)

每个人都知道多态性是继承的一大优势。我在继承中发现的另一个好处是有助于创建现实世界的复制品。例如,在付费滚动系统中,如果我们使用超类Employee继承所有这些类,我们会处理经理开发人员,办公室男生等。它使我们的程序在现实世界的背景下更容易理解,所有这些类基本上都是员工。还有一个类不仅包含它们还包含属性的方法。因此,如果我们在Employee中包含员工的通用属性 类似于社会安全号码年龄等,它将提供更多的代码重用和概念清晰度,当然还有多态性。然而,在使用继承的时候,我们应该牢记的是基本的设计原则“确定应用程序的各个方面,这些方面会有所不同,并将它们从那些变化的方面分开”。您永远不应该实现通过继承而是使用组合更改的应用程序方面。对于那些不可改变的方面,如果明显的“是一种”关系存在,你应该使用继承。

答案 4 :(得分:6)

如果符合以下情况,则首选继承:

  1. 您需要公开您扩展的类的整个API(使用委托,您需要编写许多委托方法)您的语言不提供简单的方式来表示“委托”所有未知的方法“。
  2. 您需要访问没有“朋友”概念的语言的受保护字段/方法
  3. 如果您的语言允许多重继承,则委托的优势会有所减少
  4. 如果您的语言允许在运行时动态继承类或甚至实例,则通常根本不需要委派。如果您可以同时控制暴露的方法(以及它们如何暴露),则根本不需要它。
  5. 我的结论:委托是编程语言中的错误的解决方法。

答案 5 :(得分:4)

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

答案 6 :(得分:3)

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

  

在现实世界的发展中   练习通常被称为   Programming to Interface and not to Implementation

     

接口只是合同或签名,他们不知道   关于实施的任何事情。

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

这是一个基本的例子。

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
}

alt text http://ruchitsurati.net/myfiles/interface.png

答案 7 :(得分:2)

模板方法模式怎么样?假设您有一个基础类,其中包含大量可用于自定义策略的分数,策略模式至少有以下原因之一是没有意义的:

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

  2. 这些政策是相互耦合的,因为自由地混合和匹配它们是没有意义的。它们只在所有可能组合的非常有限的子集中有意义。

答案 8 :(得分:0)

您写道:

  

[1]很多人使用古典音乐   继承实现多态   而不是让他们的课程   实现一个接口。的目的   继承是代码重用,而不是   多态性。还有一些人   使用继承来建模他们的   直观地理解“is-a”   通常可以的关系   有问题的。

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

继承是关于接口重用,而不是实现重用。关于代码重用,,如上所述。

正如你正确指出的那样,继承是为了模拟一个IS-A关系(很多人弄错了这个事实与继承本身无关)。你也可以说'BEHAVES-LIKE-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。 '输出'不是结构,而是'界面'。它可能被称为“可写”。而不是'公共输出',你会说'实现可写'。就设计而言,有什么区别?

无。

答案 9 :(得分:0)

当你问:

  

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

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

以下是我看到的继承问题:

  1. 总是必须测试派生类指针的运行时类型,看看它们是否可以被抛出(或者也可以向下)。
  2. 这种'测试'可以通过各种方式实现。您可能有某种返回类标识符的虚方法。或者你可能不得不实施RTTI(运行时类型识别)(至少在c / c ++中),这会给你带来性能损失。
  3. 未能“投射”的类类型可能存在问题。
  4. 有许多方法可以在继承树中上下转换类类型。

答案 10 :(得分:0)

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

有三种方法可以解决这个问题:

  1. 继承。
  2. 复制代码(code smell“重复代码”)。
  3. 将逻辑移至另一个类(代码闻起来“懒惰的类”,“中间人”,“消息链”和/或“不恰当的亲密关系”)。
  4. 现在,可能会滥用继承。例如,Java具有类InputStreamOutputStream。这些子类用于读/写文件,套接字,数组,字符串,还有几个用于包装其他输入/输出流。根据他们的工作,这些应该是接口而不是类。

答案 11 :(得分:0)

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

答案 12 :(得分:0)

完全不是OOP但是,组合通常意味着额外的缓存缺失。它确实依赖,但让数据更接近是一个优势。

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