>

介绍

在 C++,Java 等面向对象的语言中,继承和重载是整个语言的核心价值,而 Go 语言并非是完全面向对象的语言,不支持对同一个类定义多个同名但不同参数的函数(也即重载 overload),但是 Go 推崇一种叫做组合的设计理念,通过不同类型的组合能同样达到强大的软件设计能力,使得不同的类之间可以继承并且重写对方的函数签名相同的方法。

在本文中,将展示如何通过 Go 语言的组合设计方式来达到传统面向对象语言中的继承和覆盖效果。

我们知道,Go 语言中,struct 结构体类型的变量可以嵌套声明另外一个类型(普通类型或者结构体)做为其成员,这个内嵌的成员,有两种存在方式,一种是实名,另一种则是匿名,如果采用的是实名,那么就是(Go 语言中的组合设计模式),而如果是匿名(只有类型),那么就是继承模式。

外层结构体对象可以直接调用内嵌成员所声明的方法,而无需带成员名或类型名(匿名成员本来就只有类型名,而无成员名),而这种调用方式就像是外层结构体对象在调用自己声明的方法一样。

当外层结构体声明了一个函数与匿名内嵌成员所声明的函数同名时,再直接调用调用该函数,将会覆盖内嵌成员的同函数,也即是说,这实现了覆盖 override 效果。

如下,socketClient 用匿名的方式声明了一个 BaseService 成员变量

1
2
3
4
type socketClient struct {
BaseService
id int64
}

BaseService 内嵌的匿名成员变量,其定义了一个 Start() 方法,如下

1
2
3
4
5
6
7
8
type BaseService struct {
name string
started uint32
}

func (bs *BaseService) Start() {
fmt.Println("BaseService Start...")
}

socketClient 可以直接调用匿名的内嵌成员 BaseService 的 Start() 方法,就像是自己的方法一样。

1
2
3
4
5
6
7
8
9
func main() {
cli := &socketClient{id: 666}
cli.BaseService = &BaseService{
name: name,
started: started,
}

cli.Start()
}

利用上述这个特性,我们就可以实现类似 C++ 中的覆盖功能。因为,一旦 socketClient 自己也定义了一个与匿名成员一样的方法,再直接调用这个方法时,就会调用自己定义的,而不是成员的那个,这样就达到了重写覆盖 override 的效果。

完整例子:

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
package main

import "fmt"

type Service interface {
Start()
}

type BaseService struct {
name string
started uint32
}

func (bs *BaseService) Start() {
fmt.Println("BaseService Start...")
return
}

func NewBaseService(name string, started uint32) *BaseService {
return &BaseService{
name: name,
started: started,
}
}

type socketClient struct {
BaseService
id int64
}

func (sc *socketClient) Start() {
fmt.Println("socketClient Start() ...")
return
}

type localClient struct {
BaseService
id int64
}

func test(s Service) {
s.Start()
}

func main() {
cli := &socketClient{id: 666}
cli.BaseService = *NewBaseService("alex", 1)
test(cli)

cli1 := &localClient{id: 888}
cli1.BaseService = *NewBaseService("jack", 1)
test(cli1)

}

输出

BaseService Start… # 继承
socketClient Start() … # 覆盖

在这里,我们的设计还可以更进一步,因为在面向对象语言(C++, Java 等)中,最外层的对外接口通常是基类类型,比如做函数参数。而在 Go 语言中,最外层的通常是 interface{} 类型,而不能是 struct 类型,
简单来说就是,我们通常在 interface{} 中声明统一的调用函数,最终的调用都是通过调用子类中对 interface{} 接口所实现的函数,也就是说,我们需要达到的效果应该是根据 interface{} 接口中声明的函数,去调用子类的相应函数,而上述的继承和重写却是锚定基类 BaseService 去做的(基类声明了什么函数,子类也实现同名函数),显然不符合设计的要求。

因此,我们需要进一步改进.

因为 Service 接口的使用让我们看到了类似多态的效果,
我们可以把 Service 接口的声明看做是纯虚函数,而 BaseService 作为基类,但是我们在这个基类中声明一个 Service 类型的成员,由其子类去实现。
同时,BaseService 自身也有一个对 Service 接口的实现,在这个实现的内部,调用子类实现的相关函数!
比如 Service 接口声明了一个 Start() 函数,那么,BaseService 实现的 Start() 函数中,调用其子类实现的 Start() 函数。

这样的好处是,最终的子类,比如 xxxService 可以用虚基类 Service 中声明的统一的方法去调用。
而最终的子类的所有方法都会在 BaseService 中的方法内部被调用,也就是说,子类不需要实现任何 BaseService 中同名的方法,就可以达到重写目的。

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
type Service interface {
Start() (bool, error)
OnStart() error
}

type BaseService struct {
name string
started uint32

impl Service
}

func (bs *BaseService) Start() (bool, error) {
bs.impl.OnStart()
}

func (bs *BaseService) OnStart() error {
return nil
}

type eventSwitch struct {
BaseService
message string
}

func (evsw *eventSwitch) OnStart() error {
evsw.BaseService.OnStart()
message = "evsw message"
return nil
}

func test() {
esw := &eventSwitch{}
esw.BaseService = BaseService{
name: name,
impl: esw,
}

esw.Start()
}

func main() {
test()
}

解释:

这里 BaseService 作为基类,其实现了 Service 接口所声明的所有函数。
eventSwitch 作为最终的子类,跟最早的设计一样,eventSwitch 只是组合了 BaseService 匿名成员,并没有实现 Service 接口,但是却能够对其调用接口的所声明方法。

简单来说就是,只要子类组合了 BaseService 作为匿名成员,就可以使用 Service 声明的方法了。

关键就两点:

(1)虽然子类 eventSwitch 没有实现 Service 接口,但是却组合了 BaseService 匿名成员,这个匿名成员实现了 Service 接口所声明的方法,因此 evenSwitch 也就可以作为 Service 类型使用。
(2)在 BaseSerivce 中又声明了一个 Service 类型的成员,把 eventSwitch 保留在这个成员中,这样,BaseService 又能够动态调用子类的实现!


如果你对我的文章感兴趣,欢迎留言或者关注我的专栏。

微信公众号:“知辉”

搜索“deliverit”或

扫描二维码