9

这个问题不是像“继承与组合”这样的问题。

我完全理解继承与组合的不同之处,我知道Liskov 替换原则钻石问题,它们的优缺点,这两个概念似乎都很简单。但是到处都有很多关于继承和组合的问题,我想,也许我误解了这个简单的想法。

让我们专注于Go。Go 是一种来自 Google 的语言,每个人都很兴奋,它没有继承,没有类,但它有组合,这很酷。对我来说,Go 中的组合为您提供与其他语言(C++、Java、...)中的继承完全相同的功能 - 组件方法会自动公开并作为后续结构的方法使用,如下所示:

package main

import (
    "fmt"
)

type Car struct{
    name string
}

func (c *Car) move() bool { 
    return true
} 

type MyCar struct{
    Car   
}

func main() {
    var c MyCar
    fmt.Print(c.move())
}

综上所述,组合优于继承,因为:

  1. 更灵活(允许您在运行时更改组件,因此您可以影响“类”的工作方式。
  2. 没有钻石问题(但钻石问题是可以解决的,所以这不是很强的优势)

如果你考虑 Go 和它的接口(每个对象,具有由接口定义的方法,都隐含地实现了这个接口)你有最终的解决方案吗?我们可以说带有一些语法糖的组合可以代替继承吗?

这样的设计符合 Liskov 替换原则。我是否错过了某些东西或继承(从任何语言中知道)与 Go 中已知的组合(和接口)相比没有优势?

===== 编辑1 =====

为了澄清起见,可以Go使用“标准”组合机制,如下所示(此示例的行为与前一个类似):

package main

import (
    "fmt"
)

type Car struct{
    name string
}

func (c *Car) move() bool { 
    return true
} 

type MyCar struct{
    car Car
}

func (c *MyCar) move() bool { 
    return c.car.move()
} 

func main() {
    var c MyCar
    fmt.Print(c.move())
}

但是,如果您像前面的示例一样使用它,则所有方法都隐含在“MyCar 类”中。

4

5 回答 5

4

简短的回答

真的没有那么黑白分明。简而言之,是的。任何可以通过继承解决的情况都可以通过组合来解决。简而言之,您的问题的答案是肯定的;继承可以用组合代替。

为什么没那么简单

何时使用继承
这不是你是否可以将它们换掉的问题。这取决于您正在编程的上下文,并且更多的是您是否应该将它们换掉的问题。以 Java 中的这个简单示例为例:

public class Person
{
    // Assume every person can speak.
    public void speak()
    {
    }
}

现在,假设我们还有另一堂课,戴夫。戴夫是一个人。

public class Dave extends Person
{
     public void speak() { System.out.println("Hello!"); }
     public void killSomeone() {} // Dave is a violent Guy.
}

现在让班级Dave看起来像这样更有意义吗?

public class Dave
{
     private Person p;
     // Composition variant.

     public void speak() { p.speak(); }
     public void killSomeone() {} // Dave is a violent Guy.
}

这段代码暗示戴夫有一个人。它没有那么简单,也没有解释自己。此外,任何人能做的事,戴夫都能做,所以我们断言戴夫是一个“人”是有道理的。

何时使用合成

当我们只想公开部分类的接口时,我们使用组合。按照我们之前的示例,假设Dave有一个Guitar. 吉他有一个更复杂的界面:

public class Guitar
{
     public Color color;
     // Guitar's color.
     public Tuning tuning;
     // Guitar's tuning.

     public void tuneGuitar()
     {}

     public void playChord()
     {}

     public void setColor()
     {}
}

现在,如果我们要继承这个类,结果会是什么?

好吧,Dave 类现在将具有属性colortuning. Dave有调音吗?我想不是!这就是继承没有意义的地方。我们不想将整个Guitar接口与接口一起暴露出来Dave。我们只希望用户能够访问Dave需要访问的内容,因此在这种情况下,我们将使用一些组合:

public class Dave extends Person
{
     private Guitar guitar;
     // Hide the guitar object. Then limit what the user can do with it.

     public void changeGuitarColor(Color newColor)
     {
         // So this code makes a lot more sense than if we had used inheritance.
         guitar.setColor(newColor);
     }


     public void speak() { System.out.println("Hello!"); }
     public void killSomeone() {} // Dave is a violent Guy.
}

结论

这真的不是什么可以替代另一个的情况。这是关于您在其中实施这些技术的情况。希望在示例结束时,您会看到继承适用于一个对象IS A对象的情况,而组合用于一个对象HAS A对象的情况。

于 2013-04-01T18:51:44.893 回答
3

很抱歉发布了 necroposting,但似乎问题和答案都集中在代码重用上。我们可以同时使用继承和组合来重用来自共同“祖先”的一些代码,这是真的。组合在这里效果更好。

但是继承主要不是为了代码重用,而是为了动态绑定。“is a”关系实际上可以表述为:调用代码使用对象的接口,对特定实现一无所知。

伪代码:

// Interface
class A { 
  drawMe() {} ; 
};
// Implementations
class Rect:A { 
  drawMe() {DrawRect();}; 
};
class Circle:A { 
  drawMe() {Drawcircle();}; 
};

main() {
  // We fill the array in one part of the program
  Array_of_A arr;
  arr.add(new Rect);
  arr.add(new Circle);
  // We can use the array in completely another part
  // without any idea of what the items really are,
  // only know the interface
  foreach(item from arr) {
     item->drawMe();
  }
}

在许多语言(例如 C++)中,继承是说“这个对象实现这个接口”的唯一实用方式。而且它还允许做一些事情,比如代码重用,这真的是通过组合更好地完成。

我对 Go 一无所知,但我正确理解了您的话,它提供了另一种定义“此对象实现此接口”的方法:

每个具有由接口定义的方法的对象都隐式地实现了这个接口

所以如果你也可以通过接口调用对象,它就完成了继承的工作。但它还允许替换基类,这就是我认为他们称之为组合的原因。也许是因为它以 vtable 以外的某种方式在内部实现,但我想不出它在哪里可以产生任何影响。

因此,如果您找到一种方式说“此对象实现此接口” ,则可以用组合代替继承。

于 2013-07-20T14:18:02.633 回答
2

好问题...

当您想在语义上区分“AB”和“AB”时,您可能更喜欢继承而不是组合。例如,类 Car 可能一个 Engine 成员(组合),但可能(即继承自)一个 Vehicle。

我认为区别在于必须保持两个类共享的方法数量。继承时,只需要重写基类中真正需要的那些(虚)函数;如果你要使用纯粹的组合,你需要重新实现你的成员类的每个公共方法。

最终,“最佳”方法是最适合您当前应用的方法。

编辑:

这里有一个小而简洁的辩论: http ://www.sitepoint.com/forums/showthread.php?568744-When-to-use-inheritance

以及关于它的维基百科文章(!): http ://en.wikipedia.org/wiki/Composition_over_inheritance#Drawbacks

于 2013-04-01T18:52:28.333 回答
0

深入到核心继承只是高级语言中实现类型分支(分派)的一种便利机制。因此,所有与继承和子类型多态性相关的特殊机制都可以通过添加显式的“类型标签”并在它们上进行分支来解决。

在 Java 中,有时仅树的多态性不能很好地模拟现实世界的问题,匹配类型(instanceofpattern matching等)无处不在。您可以使用组合构建自己的类型标签来实现相同的目的。事实上,由于 Java 中枚举的效率低下,我们也将枚举手工制作为常量。

真正的问题是关于人体工程学。软件构建就是处理复杂性。如果您可以使用继承降低复杂程度,即新实体实际上是旧实体的扩展,那么为什么不使用继承呢?毕竟,如果“能做”真的是个问题,我们现在仍然在编写机器代码。之所以存在这些高级机制,是为了帮助我们更好地理解这个问题。在这个程度上,我认为用无条件的组合代替继承对我们没有任何好处。它只会给我们的项目增加更多的复杂性和样板代码,没有好处。

于 2022-01-21T06:49:58.570 回答
0

我认为这里真正的问题是继承与多态性。在像Java 这样的流行语言中,两者结合在一起,即子类型多态性只能通过实现继承来实现。Go证明了即使在静态类型语言中也可以在没有继承的情况下实现多态性(这在 Go 之前就已为人所知)。所以问题是,如果你可以在没有继承的情况下拥有多态性,你为什么要继承呢?

现代的答案似乎是......你不会。将继承与多态性混为一谈假定继承是最常见的用例。甚至 James Gosling 也承认实现继承是一个错误。这并不是说它没有用。但是继承的用例可以由框架或 DSL 覆盖,而不是强制到每个子类型关系中。在这方面,Go有机会从 Java 的错误中吸取教训。

于 2018-11-01T20:05:46.180 回答