为什么更喜欢组合而不是继承?每种方法都有哪些权衡?什么时候应该选择继承而不是组合?


当前回答

我看没有人提到钻石问题,这可能是继承带来的。

总之,如果类B和C继承了a,并且都重写了方法X,而第四个类D继承了B和C,并且没有重写X,那么应该使用X D的哪个实现?

维基百科对这个问题中讨论的主题提供了一个很好的概述。

其他回答

你想强迫自己(或其他程序员)坚持什么,什么时候你想让自己(或另一个程序员)有更多的自由。有人认为,当你想强迫某人以某种方式处理/解决某个特定问题时,继承是有帮助的,这样他们就不会走错方向。

Is-a和Has-a是一个有用的经验法则。

在Java或C#中,对象一旦实例化就不能更改其类型。

因此,如果您的对象需要显示为不同的对象或根据对象的状态或条件表现出不同的行为,那么请使用组合:参考状态和策略设计模式。

如果对象需要具有相同的类型,则使用继承或实现接口。

如果你想得到自OOP兴起以来人们一直给出的规范的教科书式答案(你可以看到很多人在这些答案中给出了答案),那么应用以下规则:“如果你有一个is-a关系,那么使用继承。如果你有个has-a关系,则使用组合”。

这是传统的建议,如果这让你满意,你可以停止阅读,继续你的快乐之路。对于其他人。。。

is-a/has-a比较存在问题

例如:

正方形是矩形,但如果矩形类具有setWidth()/setHeight()方法,那么没有合理的方法可以在不违反Liskov替换原则的情况下使正方形继承自矩形。is-a关系通常可以被改写为听起来像has-a关系。例如,员工是一个人,但一个人也具有“已就业”的就业状态。如果不小心,is-a关系可能会导致讨厌的多重继承层次结构。毕竟,英语中没有一条规则规定一个对象是一件事情。人们很快就把这条“规则”传递出去了,但有没有人试图支持它,或者解释为什么这是一个很好的启发式方法?当然,这很符合OOP应该模拟真实世界的想法,但这本身并不是采用原则的理由。

有关此主题的更多信息,请参阅StackOverflow问题。

要知道何时使用继承与组合,我们首先需要了解每种方法的利弊。

实现继承的问题

其他答案在解释继承问题方面做了很好的工作,所以我在这里尽量不深入太多细节。但是,这里有一个简短的列表:

很难遵循在基类和子类方法之间交织的逻辑。通过调用另一个可重写的方法在类中不小心地实现一个方法将导致您泄漏实现细节并破坏封装,因为最终用户可能会重写您的方法并检测您何时在内部调用它。(请参阅“有效Java”第18项)。脆弱的基础问题,简单地说,如果最终用户的代码在您尝试更改它们时碰巧依赖于实现细节的泄漏,那么它们就会损坏。更糟糕的是,大多数OOP语言默认允许继承——API设计者如果没有主动阻止人们从公共类继承,那么在重构基类时就需要格外谨慎。不幸的是,脆弱的基础问题经常被误解,导致许多人不理解维护任何人都可以继承的类需要什么。致命的死亡钻石

构图的问题

有时可能有点冗长。

就是这样,我是认真的。这仍然是一个真正的问题,有时会与DRY原则产生冲突,但一般来说,这并没有那么糟糕,至少与继承相关的无数陷阱相比。

什么时候应该使用继承?

下次当你为一个项目绘制你的UML图时(如果你这样做的话),并且你在考虑添加一些继承时,请遵循以下建议:不要。

至少目前还没有。

继承是作为实现多态性的工具出售的,但与它捆绑在一起的是这个强大的代码重用系统,坦率地说,大多数代码都不需要。问题是,一旦你公开了你的继承层次结构,你就被锁定在这种特定的代码重用风格中,即使这对于解决你的特定问题来说是过度的。

为了避免这种情况,我的两分钱是永远不要公开公开你的基类。

如果需要多态性,请使用接口。如果你需要允许人们自定义你的类的行为,通过策略模式提供显式的连接点,这是一种更易读的方式来实现这一点,另外,保持这种API的稳定性更容易,因为你完全可以控制他们可以改变和不能改变的行为。如果您试图通过使用继承来遵循开放-封闭原则,以避免向类添加急需的更新,那么就不要这样做。更新类。如果你真的拥有被雇佣来维护的代码,而不是试图在代码中添加一些东西,那么你的代码库就会更干净。如果你害怕引入bug,那么就对现有代码进行测试。如果需要重用代码,首先尝试使用组合或帮助函数。

最后,如果您决定没有其他好的选择,并且必须使用继承来实现所需的代码重用,那么您可以使用它,但是,请遵循P.A.I.L.限制继承的四条规则,以保持其正常。

将继承用作私有实现细节。不要公开您的基类,请为此使用接口。这使您可以根据自己的需要自由添加或删除继承,而无需进行重大更改。保持基类抽象。这使得将需要共享的逻辑与不共享的逻辑区分开来更加容易。隔离基础类和子类。不要让子类覆盖基类方法(使用策略模式),避免让它们期望财产/方法相互存在,使用其他形式的代码共享来实现这一点。使用适当的语言特性强制基类上的所有方法不可重写(在Java中为“final”,在C#中为非虚拟)。继承是最后的手段。

特别是“隔离”规则可能听起来有点难理解,但如果你自律,你会得到一些很好的好处。特别是,它让您可以自由地避免上面提到的与继承相关的所有主要令人讨厌的陷阱。

遵循代码要容易得多,因为它不会在基类/子类之间来回穿梭。如果您从未使任何方法可重写,则当您的方法在内部调用其他可重写方法时,不会意外泄漏。换句话说,您不会意外地破坏封装。脆弱的基类问题源于依赖意外泄漏的实现细节的能力。由于基类现在是孤立的,所以它不会比依赖于另一个via组合的类更脆弱。致命的死亡钻石不再是一个问题,因为根本不需要有多层继承。如果你有抽象基类B和C,它们都共享很多功能,只需将该功能从B和C中移出,并移入一个新的抽象基类类D。从B继承的任何人都应该更新为从B和D继承,从C继承的任何人均应该从C和D继承。由于你的基类都是私有实现细节,要弄清楚谁继承了什么,做出这些改变应该不会太难。

结论

我的主要建议是在这件事上动动脑筋。比什么时候使用继承更重要的是对继承及其相关利弊的直观理解,以及对其他可以用来代替继承的工具的良好理解(组合并不是唯一的选择。例如,策略模式是一个令人惊叹的工具,它经常被遗忘)。也许当您对所有这些工具都有了很好的、扎实的理解后,您会选择比我推荐的更频繁地使用继承,这是完全可以的。至少,您正在做出明智的决定,而不仅仅是使用继承,因为这是您知道如何做到这一点的唯一方法。

进一步阅读:

我写了一篇关于这一主题的文章,深入探讨并提供了示例。一个网页,讲述了继承所做的三种不同的工作,以及如何通过Go语言中的其他方式完成这些工作。将类声明为不可继承(例如,Java中的“final”)是很好的原因列表。乔舒亚·布洛赫(Joshua Bloch)的《有效的Java》(Effective Java)一书,第18项,讨论了组合而非继承,以及继承的一些危险。

要从新程序员的不同角度解决这个问题:

当我们学习面向对象的编程时,继承经常很早就被教授,因此它被视为一个常见问题的简单解决方案。

我有三个类都需要一些通用功能。所以如果我编写一个基类并让它们全部继承,然后它们将所有这些都有这个功能,我只需要维护一次位置

这听起来很好,但实际上它几乎从未奏效,原因如下:

我们发现,我们希望我们的类具有其他一些功能。如果我们向类添加功能的方式是通过继承,那么我们必须决定——我们是否将其添加到现有基类中,即使不是每个从其继承的类都需要该功能?我们是否创建另一个基类?但是,已经从其他基类继承的类呢?我们发现,对于从基类继承的一个类,我们希望基类的行为稍有不同。所以现在我们回过头来修改我们的基类,可能会添加一些虚拟方法,或者更糟的是,一些代码会说,“如果我是继承的类型A,那么就这样做,但如果我是传承的类型B,那么就那样做。”这是很糟糕的,原因很多。一个是,每次我们改变基类时,我们都在有效地改变每个继承的类。所以我们真的在改变A、B、C和D类,因为我们需要在A类中有一个稍微不同的行为。尽管我们认为我们很小心,但我们可能会因为与这些类无关的原因而破坏其中一个类。我们可能知道为什么我们决定让所有这些类彼此继承,但这对其他必须维护我们代码的人来说可能没有意义。我们可能会迫使他们做出一个艰难的选择——我是做一些非常丑陋和混乱的事情来做出我需要的改变(见前面的要点),还是只是重写一堆内容。

最后,我们把代码绑在一些困难的地方,除了说:“酷,我了解了继承,现在我使用了它。”这并不意味着居高临下,因为我们都这样做了。但我们都这么做了,因为没有人告诉我们不要这样做。

当有人向我解释“喜欢组合而不是继承”时,我回想了一下每次尝试使用继承在类之间共享功能时,我都意识到大多数时候它并没有真正发挥作用。

解药是单一责任原则。将其视为约束。我的班级必须做一件事。我必须能够给我的班级起一个名字,以某种方式描述它所做的一件事。(凡事都有例外,但当我们学习时,绝对规则有时会更好。)因此,我无法编写名为ObjectBaseThatContainsVariousFunctionsNeedByDifferentClasses的基类。我所需要的任何不同的功能都必须在它自己的类中,然后需要该功能的其他类可以依赖于该类,而不是从该类继承。

在过于简化的风险下,这就是组合——组合多个类一起工作。一旦我们养成了这个习惯,我们就会发现它比使用继承更灵活、更可维护、更可测试。

如果你明白其中的区别,那么解释起来就更容易了。

程序代码

这方面的一个例子是不使用类的PHP(尤其是在PHP5之前)。所有逻辑都编码在一组函数中。您可以包含包含助手函数等的其他文件,并通过在函数中传递数据来执行业务逻辑。随着应用程序的增长,这可能很难管理。PHP5试图通过提供更面向对象的设计来解决这一问题。

遗产

这鼓励使用类。继承是OO设计的三大原则之一(继承、多态性、封装)。

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

这是工作中的继承。员工“是”个人或继承自个人。所有继承关系都是“is-a”关系。Employee还隐藏Person的Title属性,意思是Employee.Title将返回Employees的Title,而不是Person。

作文

组合比继承更受欢迎。简单地说,你应该:

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

组成通常是“有”或“使用”关系。这里Employee类有一个Person。它不从Person继承,而是将Person对象传递给它,这就是它“有”Person的原因。

继承上的组合

现在,假设您要创建一个管理器类型,这样您就可以:

class Manager : Person, Employee {
   ...
}

但是,如果Person和Employee都声明了Title,那么这个示例会很好用?经理头衔应该返回“运营经理”还是“先生”?在合成中,这种模糊性得到了更好的处理:

Class Manager {
   public string Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

Manager对象由Employee和Person组成。标题行为取自雇员。这种显式组合消除了其他问题中的歧义,您将遇到更少的错误。