代码之家  ›  专栏  ›  技术社区  ›  KaptajnKold

为什么要使用继承?[关闭]

  •  58
  • KaptajnKold  · 技术社区  · 14 年前

    我知道问题是 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

    我明白继承是实现多态I C++的唯一途径。在这种情况下,显然你必须使用它。因此,我的问题仅限于Java或Ruby等语言,它们分别提供了实现多态性(接口和鸭类型)的不同方法。

    13 回复  |  直到 12 年前
        1
  •  10
  •   dsimcha    14 年前

    如果您将没有显式重写的所有内容委托给实现相同接口的其他对象(“基”对象),那么基本上您已经在组合的基础上对继承进行了绿色屏蔽,但(在大多数语言中)的详细程度和样板文件要多得多。使用组合而不是继承的目的是使您只能委托希望委托的行为。

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

        2
  •  46
  •   Jerry Coffin    14 年前

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

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

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

    可以使用继承来重新使用代码,但当/如果这样做时,通常应该是 私有的 继承不是公共继承。如果您使用的语言很好地支持委托,那么您很少有足够的理由使用私有继承,这是很有可能的。Otoh,私有继承确实支持委托(通常)不支持的一些事情。特别是,尽管多态性在本例中是一个明显的次要问题,但是它 可以 仍然是一个问题——即,对于私有继承,可以从 几乎 你想要什么,并且(假设它允许)覆盖那些不太正确的部分。

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

        3
  •  16
  •   anon    14 年前

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

        4
  •  7
  •   faisalbhagat    12 年前

    每个人都知道多态性是继承的一大优势。我在继承中发现的另一个好处是帮助创建真实世界的复制品。例如,在工资单系统中,我们处理经理、开发人员、办公室职员等,如果我们继承了所有这些类,并拥有高级员工。这使得我们的课程在现实世界中更容易理解,所有这些课程基本上都是员工。还有一件事,类不仅包含方法,还包含属性。所以如果我们在雇员中包含雇员通用的属性 类,如社会保险号年龄等,它将提供更大的代码重用和概念清晰性,当然还有多态性。然而,在使用继承的时候,我们应该记住的是基本的设计原则“识别应用程序中变化的方面,并将它们与变化的方面分开”。您不应该实现那些通过继承而改变的应用程序方面,而应该使用组合。对于那些不可改变的方面,如果一个明显的“是”关系存在的话,当然应该使用继承。

        5
  •  6
  •   Aaron Digulla    14 年前

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

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

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

        6
  •  4
  •   ChaosPandion    14 年前

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

        7
  •  3
  •   this. __curious_geek    14 年前

    接口只定义对象可以做什么,而不定义如何做。所以简单来说,接口只是契约。实现接口的所有对象都必须定义自己的合同实现。在现实世界中,这给了你 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
    }
    

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

        8
  •  2
  •   dsimcha    14 年前

    模板方法模式怎么样?假设您有一个基本类,其中包含大量可自定义策略的点, 但是 策略模式至少有一个原因是不合理的:

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

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

        9
  •  0
  •   Frerich Raabe    14 年前

    你写道:

    [1]很多人使用古典音乐 遗传实现多态性 而不是让他们上课 实现一个接口。目的 继承是代码重用,而不是 多态性。此外,有些人 使用继承为其建模 对“IS-A”的直观理解 关系往往是 有问题的

    在大多数语言中,“实现接口”和“从另一个类派生类”之间的界限非常窄。事实上,在C++语言中,如果从类A派生类B,而A是纯纯方法所组成的类,则 实现接口。

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

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

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

    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。输出“”不是结构,而是“接口”。它可能被称为“可写”。您可以说“实现可写”,而不是“公共输出”。在设计上有什么不同?

    一个也没有。

        10
  •  0
  •   Powerlord    14 年前

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

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

    1. 继承。
    2. 复制代码( code smell “重复代码”)。
    3. 将逻辑移到另一个类(代码闻起来有“懒惰类”、“中间人”、“消息链”和/或“不适当的亲密关系”)。

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

        11
  •  0
  •   Brad Barker    14 年前

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

        12
  •  0
  •   WW.    14 年前

    当你问:

    即使您知道经典继承对于实现某个模型来说并不是不正确的,那么这是否足以使您使用它而不是组合呢?

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

    我看到了一些继承问题:

    1. 总是必须测试派生类指针的运行时类型,以查看它们是否可以向上(或向下)强制转换。
    2. 这种“测试”可以通过各种方式实现。您可能有某种返回类标识符的虚拟方法。或者失败,您可能需要实现RTI(运行时类型标识)(至少在C/C++中),它可以给您一个性能命中。
    3. 未能获得“cast”的类类型可能存在潜在问题。
    4. 有很多方法可以在继承树中上下转换类类型。
        13
  •  0
  •   bestsss    14 年前

    一些完全不是OOP的东西,但是,组合通常意味着额外的缓存丢失。这取决于数据的接近程度。

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