Go 语言中的组合设计

介绍

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

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

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

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

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

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

type socketClient struct {
    BaseService
    id int64
}

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

type BaseService struct {
    name    string
    started uint32
}

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

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

func main() {
    cli := &socketClient{id: 666}
    cli.BaseService = &BaseService{
        name:    name,
        started: started,
    }
    
    cli.Start()
}

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

完整例子:

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 中同名的方法,就可以达到重写目的。

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 又能够动态调用子类的实现!

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

推荐阅读更多精彩内容