从Go语言实现模板设计模式浅谈Go的抽象能力

首先抛出一个观点,那就是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里好用的特性,那样就真是接近完美了!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,776评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,527评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,361评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,430评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,511评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,544评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,561评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,315评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,763评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,070评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,235评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,911评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,554评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,173评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,424评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,106评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,103评论 2 352