0%

Go语言中的面向对象

我们用钻头,目的不是为了钻他两下,而是为了想要一个窟窿眼。

面向对象也一样,用OOP只是手段,写出好维护的代码才是目的。

不是为了面向对象而强行面向对象,是通过吸收面向对象的精华,写更优秀的代码。

1.面向对象拆解

面向对象能流行,因为确实很优秀。

  • 可复用,不用子类多写代码,父类方法就能给子类方法复用。
  • 灵活扩展,尽管父类已经定义了主体逻辑,但子类可以自由选择怎么实现。
  • 好维护,符合开闭原则,对添加子类开放,对修改父类关闭。对子类的改动不用担心影响全局(不可能一点都不改吧)。

那go语言跟普通面向对象语言差异这么大,是怎样仍然完美拥有这些优点呢。

1.1映射

如果把 Java 类拆解到go里,属性就是struct,方法就是interface。但构造方法不在其列。

比如java类可以这样写

1
2
3
4
5
class Bird{
private String name;
public String getName(){ return this.name; }
public void fly(){}
}

go 里可以这样写

1
2
3
4
5
6
7
8
9
10
11
type IBird interface{
fly()
}
type Bird struct{
IBird
}
func (b *Bird)fly(){}
// 构造函数 返回值类型是interface
func NewBird() IBird{
return &Bird{}
}

go 语言里,返回值类型是重点。返回值类型不是定义的struct,而是interface。

那么能不能不返回定义的 interface,而是返回定义的 struct呢?

答案是不行。这就涉及到两种语言对代码复用的实现方式。

1.2继承和组合

在java这类面向对象的语言上,复用是通过继承的方式来实现的。

子类继承父类,子类完全可以代替父类来使用。

1
2
3
4
5
6
7
8
9
10
class A{
public void show(){}
}
class B extends A{}

public void letShow(data A){
data.show();
}

letShow(new B());

上述操作是完全没问题的,因为 B 也是 A的一种。

但是在go里,上述就行不通了。

go的复用是通过组合的方式来实现的。没有父类子类的概念,而是超集的概念。

超集可以执行子集的方法,但是不支持作为子集类型被传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Base struct{}
func (b Base) Show() {}

type Super struct {
Base
}

func callBase(b Base) {}

super := Super{}
// 可以执行子集的方法
super.Show()
// 但不支持作为子集类型被传入
// Cannot use 'Super{}' (type Super) as type Base
// callBase(Super{})

所以你来我往大家操作的类型都是 interface。

1.3殊途同归

但回过头仔细想想,一般情况下,Java里所有的属性都建议设为private,不对外开放。外部只能调用方法来处理。跟go里也差不多。

这种机制在java里只是写起来有些死板,但是在go里,直接就被定死了,想要灵活,想要复用就只能返回 interface。

这样一想,写java的时候念头都通达了。OOP的时候不用再想着和谁干点什么,而是想着找个能干的就行,管他是谁呢。

2.面向对象实战

众所周知,百闻不如一见,百看不如一干。所以我们以一个线上需求实践一下。

需求:将多个数据源提供的数据入库,各个数据源提交来的字段不一样,但最终落地的数据字段是一致的。

2.1 代码

下面的代码不是很规范,用了几个魔数,类型还用了map。忽略细节,看本质。

真正写代码不会有人这样写的。

真正写代码不会有人这样写的。

真正写代码不会有人这样写的。

dddd

无封装写法

1
2
3
4
5
6
7
8
9
10
11
12
func saveData(request map[string]string) {
dataToSave := ""
switch request["version"] {
case "source1":
dataToSave = extractFromSource1(request)
case "source2":
dataToSave = extractFromSource2(request)
}
if dataToSave != "" {
Save(dataToSave)
}
}

简单封装写法

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
type IExtract interface {
Extract(request map[string]string) *model.data
}
type AbstractExtractor struct{
IExtract
}
type Extractor1 struct {
AbstractExtractor
}
type Extractor2 struct {
AbstractExtractor
}

func GetExtractor(request map[string]string) IExtract {
switch(request["version"]) {
case "source1":
return &Extractor1{}
case "source2":
return &Extractor2{}
}
return nil
}

func saveData(request map[string]string) {
extractor := GetExtractor(request)
if extractor == nil{
return
}
Save(extractor.Extract(request))
}

其实还可以封装得再给力一点,比如

  • 分到不同的文件,改动一个逻辑的时候尽量不影响其他逻辑。
  • 干掉那个Switch,让他自己动(反射、map或者init)。

后面有机会再说。

2.2分析

封装了,代码反倒更长了。

所谓一寸长,一寸强,有谁会拒绝更长的呢。

复用性:只要在 AbstractExtractor 名下定义的方法, Extractor1 和 Extractor2都能调用。

灵活扩展:如果要增加一种数据源,可以采用近似于新加子类的方式操作

好维护:假如 Extractor2 和 Extractor1 某个地方不一样,自己改自己的就行了,不用担心影响全局。

上面不就是一个典型的工厂模式吗

2.3拓展

那么好好的面向对象怎么不能用了,就算用了经典的面向对象,现有的特性应该也可以完全保留。

好端端的,为什么非要用这种方式拆开呢?

业务还没写好,就不想这种终极问题了。