Go 面向对象 已翻译 100%

lidashuang 投递于 2013/03/06 00:16 (共 11 段, 翻译完成于 03-10)
阅读 8026
收藏 82
Go
7
加载中

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

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

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

Go语言中没有继承。忘记is-a的关系,而是就组合而言的面向对象设计。

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


enixyu
翻译于 2013/03/10 00:03
2

通过例子说明组合

最近阅读了一篇Ruby的面向对象编程实践, 我决定使用Go语言翻译这个例子。

Chapter 6说明了一个简单的问题。维修工需要知道自行车出行需要带上的备件,决定于哪一辆自行车已经被租出去。问题可以通过经典的继承来解决,山地车和公路自行车是自行车基类的一个特殊化例子。Chapter 8使用组合改写了同一个例子。我很高兴这个例子翻译成Go。让我们看看。

enixyu
翻译于 2013/03/10 00:15
1

Packages(包)

package main

import "fmt"

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

Types(类型)

type Part struct {
    Name        string
    Description string
    NeedsSpare  bool
}

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

type Parts []Part

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

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

lidashuang
翻译于 2013/03/10 00:10
1

方法

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.

enixyu
翻译于 2013/03/10 00:31
2

内嵌

type Bicycle struct {
    Size string
    Parts
}

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

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

与继承一起使用的模式,就像模板方法模式,并不适合于内嵌。就组合和委托而言去考虑会更好,就如我们这个例子一样。

enixyu
翻译于 2013/03/10 00:42
1

Composite Literals(复合语义)

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工厂。


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

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

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

enixyu
翻译于 2013/03/10 00:52
2

输出

    fmt.Println(roadBike.Spares())
    fmt.Println(mountainBike.Spares())
    fmt.Println(recumbentBike.Spares())
我们将以默认格式打印 Spares 的调用结果:
[{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}]

组合 Parts

    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 看上去好了一点)。

K6F
K6F
翻译于 2013/03/10 04:24
1

接口 Interfaces

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

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

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

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

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

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

[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接口。它是这么定义的:

type Stringer interface {
    String() string
}

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

super0555
翻译于 2013/03/10 10:02
1

Exports 导出

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

type Part struct {
    name        string
    description string
    needsSpare  bool
}

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

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 字段

lidashuang
翻译于 2013/03/10 00:58
1

找到一些私有性

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

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

super0555
翻译于 2013/03/10 10:17
1
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(14)

刘兰琪
刘兰琪
在函数前声明一个该类型的接受者表示该类型的方法,这样的方式很直观很简洁啊。把方法包在类大括号里,当方法比较多时候,又觉得太臃肿;声明与实现分开吧,又得多写一次声明,总比写个类型接受者变量多得多吧。这些本该是编译器干的活,无奈还需要人工来半辅助。
琼丝雨露
琼丝雨露

引用来自“无闻”的评论

翻译的不错,不过需要一定的GO基础来看这篇文章

嗯,就是就是! 不讲一下基础先
白起
白起
确实个人不喜欢继承
lidashuang
lidashuang

引用来自“切克闹”的评论

引用来自“lonun”的评论

go语言中申明方法的语法太操蛋

确实很操蛋

习惯了也挺好的
切克闹

引用来自“lonun”的评论

go语言中申明方法的语法太操蛋

确实很操蛋
自由建客
自由建客
不伦不类
简单代码
简单代码
关注go!
刀哥
刀哥
go语言中申明方法的语法太操蛋
你来人间一趟
你来人间一趟
看的很爽 多翻译几篇吧
enixyu
enixyu

引用来自“szxlucifer”的评论

翻译的第一段中“通过接口实现多台”应该是“多态”吧? @enixyu

拼音打字打错了。。呵呵。。。
返回顶部
顶部