首先抛出一个观点,那就是Go的抽象能力的确不如Java这种严格的OOP语言强。具体表现之一就是模板模式的实现。
模板的实现
模板模式是OOP编程中的一把神兵利器,用好了能够提高代码的复用程度,大大提高开发效率。例如,我们可以在父类中定义完成一个任务的几个步骤并分别给出一个默认实现,然后子类继承父类,子类只需要重写自己感兴趣的方法即可,剩余逻辑都可以复用父类的代码。Spring源码中就大量充斥着这样的套路。但是在go语言中,连类都没有,更别提继承了,那如何才能使出这种套路呢?答案就是内嵌匿名结构体。
如果一个struct A中内嵌了另一个匿名的struct B, 那么A就可以【直接】访问B中所有的字段和方法。这句话翻译成代码就是这个样子的:
type B struct {
bField string
}
func (b *B) bMethod() {
}
type A struct {
B // A可以【直接】访问B里所有字段和方法
}
a := new(A)
a.bField // OK
a.bMethod() // OK
这就是Go语言间接实现继承的唯一方法,内嵌匿名结构体。如果在定义A时给B进行了命名,比如b, 那调用时就只能a.b.bField()
, a.b.bMethod()
了,完全失去了继承的意义。
实现模板模式最核心的问题在于,如何将事先定义好的步骤【延迟】到子类中执行。在Java中,这是通过编译器和JVM共同保证的,也就是说不需要程序员关心此事。但是在go中不存在类似于abstract
, extends
这样的关键字,那就需要我们通过编写代码来保证父类定义的方法在子类中执行这一条件。具体思路为,在"父"结构体中定义多个能够共同完成任务的函数类型字段,"子"结构体在内嵌"父"结构体时,将"父"结构体的这些函数类型字段赋值为自己的方法实现即可。例如,我们有模板结构体TaskTemplate
, 它有beforeTask func()
和afterTask()
两个函数字段:
// 任务模板类, 定义一个执行的执行步骤
type TaskTemplate struct {
// "子类"给此字段赋值
beforeTask func()
// "子类"给此字段赋值
afterTask func()
}
然后再定义一个将所有函数组合起来的runTask()
方法:
func (task *TaskTemplate) inTask() {
fmt.Println("in task")
}
// 调用所有任务步骤
func (task *TaskTemplate) runTask() {
task.beforeTask()
task.inTask()
task.afterTask()
}
最后我们再来定义实际执行任务的MyTaskTemplate
结构体:
// 具体执行任务的构造体
type MyTaskTemplate struct {
// "继承"模板类
TaskTemplate
}
// 实现模板中的beforeTask方法
func (my *MyTaskTemplate) beforeTask() {
fmt.Println("my before task")
}
// 实现模板中的afterTask方法
func (my *MyTaskTemplate) afterTask() {
fmt.Println("my after task")
}
// 构造一个MyTaskTemplate结构体
func NewMyTaskTemplate() *MyTaskTemplate {
myTask := new(MyTaskTemplate)
// 将"父类"中的函数字段设为自己的实现
myTask.TaskTemplate.beforeTask = myTask.beforeTask
myTask.TaskTemplate.afterTask = myTask.afterTask
return myTask
}
这里重点就在于充当构造函数的NewMyTaskTemplate()
函数,在这里面我们完成了将"父类"中的"方法"替换成自己实现的任务。现在就可以执行一下了:
func TestExtends(t *testing.T) {
task := NewMyTaskTemplate()
task.runTask()
}
输出:
my before task
in task
my after task
可以看到,我们创建的是充当子类的MyTaskTemplate
结构体变量,但in task
这行输出是在"父类"中完成的,其他两行输出则是才是由"子类"完成的。那么这还有一个问题,直接创建子结构体其实达不到模板的意义,我们希望能使用一个通用的类型来引用MyTaskTemplate
,然后只调用此能用类型的runTask()
方法即可。这里显然不能使用TaskTemplate
类型的变量来引用MyTaskTemplate
,因为他们并不是同一个类型,go语言里是没有继承的概念的,我们只是做了一个简单的结构体嵌套而已。要想达到这一目的,就必须再定义一个包含了runTask()
方法的接口:
type RunTask interface {
runTask()
}
然后就可以使用此接口类型来引用任何一个实现了runTask()
方法的结构体了:
func TestExtends(t *testing.T) {
task := NewMyTaskTemplate()
invokeRunTask(task)
}
func invokeRunTask(task RunTask) {
task.runTask()
}
最后输出是完全一样的。
Go的抽象能力不够强
至此我们使用Go语言【勉强】实现了模板模式。但这是十分不优雅的,问题很多:
- 编译器无法强制"子类"来实现"父类"定义的步骤方法, 编写"子类"有可能会忘记实现,但这一错误要到运行时才能被发现。
- 需要在"子类"中手动替换父类函数变量的值。如果忘了或者根本没有使用NewXXXX()方法而是直接
&TaskTemplate{}
的话,错误也是要运行时才能发现的。
上面的两个问题完全无解,无优雅解。
Go语言的设计哲学是简单和简洁,即使用最少的关键字、最少的语法来实现最常用的功能。这一点在协程上体现的淋漓尽致,比如无需关心协程调度,无需考虑是否block线程, 同步的代码实际为异步执行和极简的go
关键字等。但这样也是有代价的,那就是牺牲了抽象能力:
砍掉了继承,导致不容易优雅实现各种设计模式;
砍掉了重载,导致无法使用同一函数名来精简需要不同参数的情况;
砍掉了泛型,导致要么interface{}
漫天飞要么重复编码;
当然,这些问题也不会让go走不了路,最多也就是走的姿势不够优雅而已。很多崇尚极简的人会有一种卸下语法糖包袱返璞归真的感觉。个人希望官方能在go 2.0的大版本升级中做一下改善,在保证语言简单、简洁的前提下稍微支持一点OOP里好用的特性,那样就真是接近完美了!
网友评论