可以通过组合完全替换继承吗?

Woj*_*ilo 8 language-agnostic oop inheritance design-patterns composition

这个问题不是 "遗产与构成"之类的问题.

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

让我们关注Go.Go是一种来自谷歌的语言,每个人都很兴奋它没有继承,它没有类,但它有组成,这很酷.对我来说,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())
}
Run Code Online (Sandbox Code Playgroud)

所以总结一下,组合比继承更好,因为:

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

如果您考虑Go及其接口(每个对象,具有接口定义的方法,实现此接口隐含),您是否拥有最终解决方案?我们可以说含有一些语法糖的成分可以代替遗传吗?

这种设计符合Liskov替代原则.我是否会遗漏某些内容或继承(从任何语言中获知)与Go中已知的组合(和接口)相比没有优势?

===== edit1 =====

为了澄清,可以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())
}
Run Code Online (Sandbox Code Playgroud)

但是如果你像前面的例子一样使用它,所有的方法都可以在MyCar类中使用.

chr*_*her 7

简短的回答

这确实不是那么黑白分明。简而言之,是的。任何可以通过继承解决的情况都可以通过组合来解决(足够接近)。简而言之,你的问题的答案是肯定的;继承可以用组合来代替。

为什么事情没那么简单

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

public class Person
{
    // Assume every person can speak.
    public void speak()
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

现在,类Dave看起来像这样更有意义吗?

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

     public void speak() { p.speak(); }
     public void killSomeone() {} // Dave is a violent Guy.
}
Run Code Online (Sandbox Code Playgroud)

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

何时使用组合

当我们只想公开类接口的一部分时,我们可以使用组合。按照我们前面的示例,假设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()
     {}
}
Run Code Online (Sandbox Code Playgroud)

现在,如果我们继承这个类,结果会怎样呢?

那么,Dave 类现在将具有属性colortuningDave有调音吗?我想不是!这就是继承没有意义的地方。我们不想将整个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.
}
Run Code Online (Sandbox Code Playgroud)

结论

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

  • @danilo2我认为你的问题只是特定于Go的语言,而ChrisCooney的答案是特定于Java的语言。我们都必须编写某些东西,如果该东西没有您描述的类似 Go 的功能,那么在许多情况下就有一个很大的理由使用继承。我认为您无法像您希望的那样轻松地将语言与问题分开。 (2认同)