Go面向对象简述

Go面向对象简述

Go 是一个完全面向对象的语言。例如,它允许基于我们的类型定义方法,而没有像其他语言一样的装箱/拆箱操作。

Go 没有使用 classes,但提供很多相似的功能:

  • 通过嵌入实现的自动消息委托
  • 通过接口实现多态
  • 通过 exports 实现的命名空间

Go 语言中没有继承。忘记 is-a 的关系,Go 采用组合的方式面向对象设计。

“使用经典的继承始终是可选的;每个问题都可以通过其他方法得到解决” - Sandi Metz

1、例子说明组合

维修工需要知道自行车出行需要带上的备件,决定哪一辆自行车出租出去。问题可以通过经典的继承来解决,山地车和公路自行车是自行车基类的一个特殊化例子。

2、Packages(包)

1
2
3
package main

import "fmt"

包提供了命名空间概念,main() 函数是这个包的入口函数,fmt 包提供格式化功能。

3、Types(类型)

1
2
3
4
5
type Part struct {
Name string
Description string
NeedsSpare bool
}

我们定义了一个新的类型名为 Part,非常像 C 的结构体

1
type Parts []Part

Parts 类型是包含 Part 类型的数组切片,Slice 可以理解为动态增长的数组,在 Go 中是很常见的。

我们可以在任何类型上声明方法,所以我们不需要要再去封装 []Part,这意味着 Parts 会拥有 slice 的所有行为,再加上我们自己定义的行为方法。

4、方法

1
2
3
4
5
6
7
8
func (parts Parts) Spares() (spares Parts) {
for _, part := range parts {
if part.NeedsSpare {
spares = append(spares, part)
}
}
return spares
}

Go 中定义方法就像一个函数,除了它有一个显式的接收者,紧接着 func 之后定义。

方法的主体十分简单。我们重复 parts,忽略索引的位置 (_),过滤 parts 后返回。append builtin 需要分配和返回一个大的切片,因为我们并没有预先分配好它的容量。

这段代码没有 ruby 代码来得优雅。在 Go 语言中有过滤函数,但它并非是 builtin。

5、内嵌

1
2
3
4
type Bicycle struct {
Size string
Parts
}

自行车由 Size 和 Parts 组成。没有给 Parts 指定一个名称,我们是要保证实现内嵌。这样可以提供自动的委托,不需特殊的声明,例如 bike.Spares() 和 bike.Parts.Spares() 是等同的。

如果我们向 Bicycle 增加一个 Spares() 方法,它会得到优先权,但是我们仍然引用嵌入的 Parts.Spares()。这跟继承十分相似,但是内嵌并不提供多态。Parts 的方法的接收者通常是 Parts 类型,甚至是通过 Bicycle 委托的。

与继承一起使用的模式,就像模板方法模式,并不适合于内嵌。

6、Composite Literals(复合语义)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var (
RoadBikeParts = Parts{
{"chain", "10-speed", true},
{"tire_size", "23", true},
{"tape_color", "red", true},
}

MountainBikeParts = Parts{
{"chain", "10-speed", true},
{"tire_size", "2.1", true},
{"front_shock", "Manitou", false},
{"rear_shock", "Fox", true},
}

RecumbentBikeParts = Parts{
{"chain", "9-speed", true},
{"tire_size", "28", true},
{"flag", "tall and orange", true},
}
)

Go 提供优美的语法,来初始化对象,叫做 composite literals。使用像数组初始化一样的语法,来初始化一个结构,使得我们不再需要 ruby 例子中的 Parts 工厂。

Composite literals(复合语义)同样可以用于字段:值的语法,所有的字段都是可选的。

简短的定义操作符 (:=) 通过 Bicycle 类型,使用类型推论来初始化 roadBike 和其他。

1
2
3
4
func main() {
roadBike := Bicycle{Size: "L", Parts: RoadBikeParts}
mountainBike := Bicycle{Size: "L", Parts: MountainBikeParts}
recumbentBike := Bicycle{Size: "L", Parts: RecumbentBikeParts}

7、输出

1
2
3
fmt.Println(roadBike.Spares())
fmt.Println(mountainBike.Spares())
fmt.Println(recumbentBike.Spares())

我们将以默认格式打印 Spares 的调用结果:

1
2
3
[{chain 10-speed true} {tire_size 23 true} {tape_color red true}]
[{chain 10-speed true} {tire_size 2.1 true} {rear_shock Fox true}]
[{chain 9-speed true} {tire_size 28 true} {flag tall and orange true}]

8、组合 Parts

1
2
3
4
5
6
7
8
9
10
    ...

comboParts := Parts{}
comboParts = append(comboParts, mountainBike.Parts...)
comboParts = append(comboParts, roadBike.Parts...)
comboParts = append(comboParts, recumbentBike.Parts...)

fmt.Println(len(comboParts), comboParts[9:])
fmt.Println(comboParts.Spares())
}

Parts 的行为类似于 slice。按照长度获取切片,或者将数个切片结合。Ruby 中的类似解决方案就数组的子类,但是当两个 Parts 连接在一起时,Ruby 将会“错置” spares 方法。

“……在一个完美的面向对象的语言,这种解决方案是完全正确的。不幸的是,Ruby 语言并没有完美的实现……” —— Sandi Metz

在 Ruby 中有一个难看的解决方法,使用 Enumerable、forwardable,以及 def_delegators。Go 没有这样的缺陷,[]Part 正是我们所需要的,且更为简洁(更新:Ruby 的 SimpleDelegator 看上去好了一点)。

9、接口 Interfaces

Go 的多态性由接口提供。不像 JAVA 和 C#,它们是隐含实现的,所以接口可以为不属于我们的代码定义。

和动态类型比较,接口是在它们声明过程中静态检查和说明的,而不是通过写一系列响应(respond_to)测试完成的。

“不可能不知不觉的或者偶然的创建一个抽象;在静态类型语言中定义的接口总是有倾向性的。” - Sandi Metz

给个简单的例子,假设我们不需要打印 Part 的 NeedsSpare 标记。我们可以写这样的字符串方法:

1
2
3
func (part Part) String() string {
return fmt.Sprintf("%s: %s", part.Name, part.Description)
}

然后对上述 Print 的调用将会输出这样的替代结果:

1
2
3
[chain: 10-speed tire_size: 23 tape_color: red]
[chain: 10-speed tire_size: 2.1 rear_shock: Fox]
[chain: 9-speed tire_size: 28 flag: tall and orange]

这个机理是因为我们实现了 fmt 包会用到的 Stringer 接口。它是这么定义的:

1
2
3
type Stringer interface {
String() string
}

接口类型在同一个地方可以用作其它类型。变量与参数可以携带一个 Stringer,可以是任何实现 String() string 方法签名的接口。

10、Exports 导出

Go 使用包来管理命名空间,要使某个符号对其他包(package )可见(即可以访问),需要将该符号定义为以大写字母开头,当然如果以小写字母开头,那就是私有的,包外不可见。

1
2
3
4
5
type Part struct {
name string
description string
needsSpare bool
}

为了对 Part 类型应用统一的访问原则(uniform access principle),我们可以改变 Part 类型的定义并提供 setter/getter 方法,就像这样:

1
2
3
4
5
6
7
func (part Part) Name() string {
return part.name
}

func (part *Part) SetName(name string) {
part.name = name
}

这样可以很容易的确定哪些是 public API,哪些是私有的属性和方法,只要通过字母的大小写。(例如 part.Name() vs .part.name)

注意,我们不必要对 getters 加前 Get,(例如.GetName),Getter 不是必需,特别是对于字符串,当我们有需要时,我们可以使用满足Stringer 类型接口的自定义的类型去改变 Name 字段。

11、找到一些私有性

私有命名(小写字母)可以从同一个包的任何地方访问到,即使是包含了跨越多个文件的多个结构。如果你觉得这令人不安,包也可以像你希望的那么小。

可能的情况下用(更稳固的)公共 API 是一个好的实践,即使是来自经典语言的同样的类中。这需要一些约定,当然这些约定可以应用在 GO 中。

12、最大的好处

组合、内嵌和接口提供了 Go 语言中面向对象设计的强大工具。

习惯 Go 需要思维的改变,当触及到 Go 对象模型的力量时,我非常高兴的吃惊于 Go 代码的简单和简洁。

参考文章:
Go 面向对象

评论

:D 一言句子获取中...

加载中,最新评论有1分钟缓存...