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

时间:2013-04-01 18:31:42

标签: oop design-patterns inheritance language-agnostic composition

这个问题问题,如“遗产与作曲”。

我完全理解继承与组合的区别,我知道Liskov substitution principlediamond problem,它们的优点和缺点,这两个概念似乎都很简单。但到处都有关于继承和构成的问题,我想,也许我误解了这个简单的想法。

让我们关注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())
}

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

  1. 更灵活(允许您在运行时更改组件,因此可以影响“类”的工作方式。
  2. 没有钻石问题(但钻石问题是可以解决的,所以这不是强大的优势)
  3. 如果您考虑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())
    }
    

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

4 个答案:

答案 0 :(得分: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有一个人。它不是那么简单,也不能解释自己。此外,任何人都可以做,Dave可以做,所以我们断言戴夫是一个“人”是有意义的。

何时使用作文

当我们只想暴露部分类的界面时,我们使用Composition。按照上一个示例,假设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对象和一个对象{{1}时使用组合的情况。对象。

答案 1 :(得分: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的某种方式在内部实现,但我无法想到它可以产生什么不同。

因此,如果您找到说“此对象实现此接口”的方法,继承可以由合成替换。

答案 2 :(得分:2)

好问题......

当你想要在“A a B”和“A a B”之间进行语义区分时,你可能希望更喜欢继承而非组合。例如。 class Car可以拥有引擎成员(组合),但可以 (即从其继承)车辆。

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

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

编辑:

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

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

答案 3 :(得分:0)

我认为真正的问题是继承与多态。在Java之类的流行语言中,两者是结合在一起的,即子类型多态性只能通过实现继承来实现。 Go 证明即使在静态类型的语言中,多态性也无需继承即可实现(这在Go之前是已知的)。所以问题是,如果您可以不继承而拥有多态性,为什么要具有继承性呢?

现代的答案似乎是……您不会。将继承与多态性混合起来会假定继承是最常见的用例。甚至James Gosling也承认implementation inheritance was a mistake。但这并不是说它没有用。但是继承的用例可以由框架或DSL覆盖,而不必强加于每个子类型关系中。在这方面, Go 有机会向Java的错误学习。