官方关于golang的继承和重载的FAQ#
原文部分来自:https://segmentfault.com/a/1190000022429780
面向对象的编程,至少在最著名的语言中,涉及对类型之间关系的过多讨论,这些关系通常可以自动派生。Go 采取了不同的方法。
与其要求程序员提前声明两种类型是相关的,在 Go 中,类型会自动满足任何指定其方法子集的接口。除了减少簿记之外,这种方法还有真正的优势。类型可以同时满足多个接口,没有传统多重继承的复杂性。接口可以是非常轻量级的——具有一个甚至零个方法的接口可以表达一个有用的概念。如果出现新想法或用于测试,可以事后添加接口——无需注释原始类型。因为类型和接口之间没有明确的关系,所以没有要管理或讨论的类型层次结构。
可以使用这些想法来构建类似于类型安全的 Unix 管道的东西。例如,了解如何fmt.Fprintf
为任何输出启用格式化打印,而不仅仅是文件,或者bufio
包如何与 文件 I/O 完全分离,或者image
包如何生成压缩图像文件。所有这些想法都源于io.Writer
表示单个方法 ( Write
)的单个接口( )。而这只是皮毛。Go 的接口对程序的结构有着深远的影响。
这需要一些时间来适应,但这种隐式的类型依赖是 Go 最高效的事情之一。
–faq: https://golang.org/doc/faq#inheritance
如果不需要进行类型匹配,则方法分派会得到简化。使用其他语言的经验告诉我们,拥有多种名称相同但签名不同的方法有时很有用,但在实践中也可能会令人困惑和脆弱。仅按名称匹配并要求类型的一致性是 Go 类型系统中一个主要的简化决定。
关于运算符重载,它似乎更方便而不是绝对要求。同样,没有它,事情会更简单。
– faq:https://golang.org/doc/faq#overloading
从一个案例引入#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| type ShapeInterface interface {
Area() float64
GetName() string
PrintArea()
}
type Shape struct {
name string
}
func (s *Shape) GetName() string {
return s.name
}
func (s *Shape) Area() float64 {
return 0.0
}
func (s *Shape) PrintArea() {
fmt.Printf("%s : Area %v\r\n", s.GetName(), s.Area())
}
// Rectangle 矩形求面积
type Rectangle struct {
Shape
w, h float64
}
func (r *Rectangle) Area() float64 {
return r.w * r.h
}
// Circle 圆形 : 重新定义 Area 和PrintArea 方法
type Circle struct {
Shape
r float64
}
func (c *Circle) Area() float64 {
return c.r * c.r * math.Pi
}
func (c *Circle) PrintArea() {
fmt.Printf("%s : Area %v\r\n", c.GetName(), c.Area())
}
func main() {
s := Shape{name: "Shape"}
c := Circle{Shape: Shape{name: "Circle"}, r: 10}
r := Rectangle{Shape: Shape{name: "Rectangle"}, w: 5, h: 4}
listshape := []ShapeInterface{&s, &c, &r}
for _, si := range listshape {
si.PrintArea() //!! 猜猜哪个Area()方法会被调用 !!
}
}
out:
Shape : Area 0
Circle : Area 314.1592653589793
Rectangle : Area 0 // 为啥这里没有调用 5 * 4
|
原因分析:Rectangle
通过组合Shape
获得的PrintArea()
方法并没有去调用Rectangle
实现的Area()
方法,而是去调用了Shape
的Area()
方法。Circle
是因为自己重写了PrintArea()
所以在方法里调用到了自身的Area()
。
解决方案#
1.将要使用的值抽取成一个方法,在初始化中进行赋值。#
定义了一个类似InitShape
的方法来完成初始化流程,这里我把ShapeInterface
接口和Shape
类型做一些调整会更好理解一些。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| type ShapeInterface interface {
Area() float64
GetName() string
SetArea(float64)
}
type Shape struct {
name string
area float64
}
...
func (s *Shape) SetArea(area float64) {
s.area = area
}
func (s *Shape) PrintArea() {
fmt.Printf("%s : Area %v\r\n", s.name, s.area)
}
...
func InitShape(s ShapeInterface) error {
area, err := s.Area()
if err != nil {
return err
}
s.SetArea(area)
...
}
|
对于Rectangle
和Circle
这样的组合Shape
的类型,只需要按照自己的计算面积的公式实现Area()
,SetArea()
会把Area()
计算出的面积存储在area
字段供后面的程序使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| type Rectangle struct {
Shape
w, h float64
}
func (r *Rectangle) Area() float64 {
return r.w * r.h
}
r := &Rectangle {
Shape: Shape{name: "Rectangle"},
w: 5, 4
}
InitShape(r)
r.PrintArea()
|
2.按照接口的定义,实现所有相关联的方法#
1
2
3
4
5
6
7
8
9
10
11
12
13
| // Rectangle 矩形求面积 : 重新定义了 Area 方法
type Rectangle struct {
Shape
w, h float64
}
func (r *Rectangle) Area() float64 {
return r.w * r.h
}
// 实现 PrintArea 方法,
func (r *Rectangle) PrintArea() {
fmt.Printf("%s : Area %v\r\n", r.GetName(), r.Area())
}
|
3.用组合的方式去减少代码量(推荐)#
面向接口编程
- 接口里面的方法之间不应该存在相互依赖,应该用单一的方法组合去完成你的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| type ShapeInterface interface {
GetName() string
GetArea() float64
}
type Shape struct {
name string
}
func (s *Shape) GetName() string {
return s.name
}
func (s *Shape) GetArea() float64 {
return 0.0
}
// Rectangle 矩形求面积 : 重新定义了 Area 方法
type Rectangle struct {
Shape
w, h float64
}
func (r *Rectangle) GetArea() float64 {
return r.w * r.h
}
// Circle 圆形 : 重新定义 Area 和PrintArea 方法
type Circle struct {
Shape
r float64
}
func (c *Circle) GetArea() float64 {
return c.r * c.r * math.Pi
}
// PrintAreaInterface 抽象出一个输出面积的接口
type PrintAreaInterface interface {
PrintArea(ShapeInterface)
}
type PrintAreas struct {
}
func (p *PrintAreas) PrintArea(shape ShapeInterface) {
fmt.Printf("%s : Area %v\r\n", shape.GetName(), shape.GetArea())
}
func NewPrintAreas() PrintAreaInterface {
return &PrintAreas{}
}
func main() {
s := Shape{name: "Shape"}
c := Circle{Shape: Shape{name: "Circle"}, r: 10}
r := Rectangle{Shape: Shape{name: "Rectangle"}, w: 5, h: 4}
listshape := []ShapeInterface{&s, &c, &r}
areas := NewPrintAreas()
for _, si := range listshape {
areas.PrintArea(si)
}
}
|
方案一#
优点
缺点
方案二#
优点
缺点
方案三(推荐)#
优点
缺点
由于本人之前是Python转到golang,对于面向对象的理解和golang的设计不太相符,应该是golang不是纯面向对象的语言,文章开头的几个FAQ也阐述了golang对面向对象支持的问题,所以在代码设计和抽象的时候不能完全的按照面向对象的方式去思考,golang应该是组合,而不是继承。和朋友探讨下来最终实现了方案三(推荐),面向接口编程的哲学。