转自:http://hackthology.com/golangzhong-de-mian-xiang-dui-xiang-ji-cheng.html
Golang的面向对象机制与Smalltalk或者Java等传统面向对象的编程语言不同。传统面向对象编程语言的一个重要特性是继承机制。因为继承机制支持在关联对象间进行代码复用和数据共享。继承机制曾在代码复用和数据共享的设计模式占据主导地位,但是目前组合这一古老的技术重新焕发了活力。
本篇文章转自Tim Henderson的 "Object Oriented Inheritance in Go", 原文地址是http://hackthology.com/object-oriented-inheritance-in-go.html。非常感谢李浩和骏奇对于这篇文章的翻译。
在我们探讨如何在Go中实现继承机制之前(Golong中的继承机制和其他语言(Java)的继承机制有区别),我们先看一下Java中如何实现继承机制。
继承与组合
让我们先看一下我最喜欢的话题之一:编译器!编译器由管道转换构成,该管道读取text文本并将其转化为机器代码、汇编语言、字节码或者其他的编程语言。管道首先会使用语法分析器对目标变成语言进行语法分析。一般情况下文本会被分解为不同的组成部分,例如:关键词、标识符、标点和数字等等。每个组成部分都会被相应的数据类型标记。例如下面这个Java数据类型:
public class Main {}
这些组成部分(可以称作标记)如下所示:
这些标记可以划分为两个部分:
- 标记类型
- 语义部分
这会导致我们进行如下的Java设计方式:
对于一些标记类型来说,例如数值常量,标记类型最好能够将包含这些属性信息。就数值常量来说,在他的标记类型里应该包括常量值这一属性。实现这一设计的传统方式是使用继承机制产生Token子类。
Inheritance and Composition in Go
Go中实现组合是一件十分容易的事情。简单组合两个结构体就能够构造一个新的数据类型。
这就是Go中实现代码和数据共享的常用方式。然而如果你想实现继承机制,我们该如何去做?
Why would you want to use inheritance in go
一个可选的方案是将Token设计成接口类型。这种方案在Java和Go都适用:
这样分析器就可以返回满足Match 和IntegerConstant类型的Token接口。
继承机制的简化版
上面的实现方案的一个问题是*IntegerConstant的方法调用中,出现了重复造轮子的问题。但是我们可以使用Go内建的嵌入机制来避免此类情况的出现。嵌入机制(匿名嵌入)允许类型之前共享代码和数据。 struct { Token value IntegerConstant中匿名嵌入了Token类型,使得IntegerConstant"继承"了Token的字段和方法。很酷的方法!我们可以这样写代码:(可以在这里试一下 :https://play.golang.org/p/PJW7VShpE0)
我们没有编写Type()和Value()方法的代码,但是*IntegerConstant也实现了Token接口,非常棒。
结构体的"继承"机制
Go中有三种方式完成”继承“机制,您已经看到了第一种实现方式:在结构体的第一个字段匿名嵌入接口类型。你还可以利用结构体实现其他两种”继承“机制: 1. 匿名嵌入结构体实例在以上的方案中,你不能嵌入与嵌入类型相同的方法名。例如结构体Bar匿名嵌入结构体Foo后,就不能拥有名称为Foo的方法,同样也不能实现type Fooer interface { Foo() }接口类型。
共享代码、共享数据或者两者兼得
相比于Java,Go在继承和聚合之间的界限是很模糊的。Go中没有extends关键词。在语法的层次上,继承看上去与聚合没有什么区别。Go中聚合跟继承唯一的不同在于,继承自其他结构体的struct类型可以直接访问父类结构体的字段和方法。
(可以试一下https://play.golang.org/p/Pmkd27Nqqy)
输出:
嵌入式继承机制的的局限
相比于Java, Go的继承机制的作用是非常有限的。有很多的设计方案可以在Java轻松实现,但是Go却不可能完成同样的工作。让我们看一下:
Overriding Methods
上面的Pet例子中,Dog类型重载了Speak()方法。然而如果Pet有另外一个方法Play()被调用,但是Dog没有实现Play()的时候,Dog类型的Speak()方法则不会被调用。 package main import ( "fmt" ) struct { name struct { Pet Breed *Pet) Play() { fmt.Println(p.Speak()) } string { func main() { d "pointer"} fmt.Println(d.Name()) fmt.Println(d.Speak()) d.Play() }(试一下https://play.golang.org/p/id-aDKW8L6)
spot my name is spot and I am a pointer my name is spot
但是Java中就会像我们预想的那样工作:
这个明显的区别是因为Go从根本上阻止了抽象方法的使用。让我们看看下面这个例子:
(试一下https://play.golang.org/p/9iIb2px7jH)
spot my name is spot and I am a pointer my name is spot and I am a pointer
现在跟我们预想的一样了,但是跟Java相比略显冗长和晦涩。你必须手工重载方法签名。而且,代码在结构体未正确初始化的情况下会崩溃,例如当调用Speak()时,speaker()却没有完成初始化工作的时候。
Subtyping
在Java中,Dog继承自Pet,那么Dog类型就是Pet子类。这意味着在任何需要调用Pet类型的场景都可以使用Dog类型替换。这种关系称作多态性,但Go的结构体类型不存在这种机制。 让我们看下面的例子:(试一下https://play.golang.org/p/e1Ujx0VhwK)
prog.go:62: cannot use d (type *Dog) as type *Pet in argument to Play
然而,接口类型中存在子类化的多态机制!
所以接口类型可以用来实现子类化的机制。但是如果你想正确的实现方法重载,需要了解以上的技巧。
Conclusion
事实上,虽然这不是Go的主打特性,但是Go语言在结构体嵌入结构体或者接口方面的能力确实为实际工作增加了很大的灵活性。Go的这些特性为我们解决实际问题提供了新的解决方案。但是相较于Java等语言,由于Go缺少子类化和方法重载支持还有存在一些局限性。Go含有一项Java没有的特性--接口嵌入。关于接口嵌入的细节请参考Golang的官方文档的Embedding部分。