Go语言中的代码重用 - 继承还是组合?
故事要从我在一个项目中,想要假装的专业一点而遇到的一个陷阱说起。
代码重用
在这个项目中,我们已经有了类似如下的代码:
package main import ( "fmt" ) func main() { user := &User{name: "Chris"} user.sayHi() } type User struct { name string } func (u *User) sayHi() { u.sayName() u.sayType() } func (u *User) sayName() { fmt.Printf("I am %s.", u.name) } func (u *User) sayType() { fmt.Println("I am a user.") }
I am Chris.I am a user.
然后我接到的新需求是这样的,我需要开发一种新的用户,它和当前这种用户有一些相同的行为。当然,最主要的是也有很多不同的行为。作为一名老司机,我当然知道,这些不同的地方才是我需要重点关注并且实现的。 为了区分这两种用户,我们就叫他们普通用户和文艺用户吧。 因为我们已经有了普通用户的实现代码了,作为一个资深(误)Java工程师,我想通过继承这个普通用户来实现代码的复用。然而悲伤辣么大,我发现在Go语言中是不支持继承的。
嵌入类型
好吧,只要思想不滑坡,办法总比困难多。我发现在Go中有一种叫做Embedding的东西。在网上的一些文章中,他们说这就是Go中实现继承的方式。可是看起来,这更像是Java中的组合,至少语法上像,是不?
package main import ( "fmt" ) func main() { artisticUser := &ArtisticUser{User: &User{name: "Chris"}} artisticUser.sayName() artisticUser.sayType() } type User struct { name string } func (u *User) sayHi() { u.sayName() u.sayType() } func (u *User) sayName() { fmt.Printf("I am %s.", u.name) } func (u *User) sayType() { fmt.Println("I am a user.") } type ArtisticUser struct { *User } func (u *ArtisticUser) sayType() { fmt.Println("I am an artistic user.") }
I am Chris.I am an artistic user.
干得漂亮!这样我就可以复用User的sayName方法,只要把sayType方法用我自己的逻辑实现就好了。这正是我想要的。
继承?组合?
但是,少侠请留步!我们试一下sayHi方法看看会发生什么?
package main import ( "fmt" ) func main() { artisticUser := &ArtisticUser{User: &User{name: "Chris"}} artisticUser.sayHi() } type User struct { name string } func (u *User) sayHi() { u.sayName() u.sayType() } func (u *User) sayName() { fmt.Printf("I am %s.", u.name) } func (u *User) sayType() { fmt.Println("I am a user.") } type ArtisticUser struct { *User } func (a *ArtisticUser) sayType() { fmt.Println("I am an artistic user.") }
I am Chris.I am a user.
这不科学!在Java里,子类总是会调用自己的方法的(已经override了父类的方法)。除非子类没有覆盖父类的方法,才会使用从父类继承来的方法。 在这个例子中,我override了(其实Go中没有这个概念)sayType方法,但是当我们在sayHi中调用它时,却没有调用这个override方法,而是用了父类的原始方法。
实际上,类型嵌入不是继承。它只是某种形式上的语法糖而已。在面向对象编程中,子类应该是可以被当做父类来使用的。在里氏替换原则中,子类应该能在任何需要的地方替换掉父类。(注意一点,我们这里一开始尝试覆盖父类的非抽象方法已经违背了里氏替换原则)。 但是在上边的例子中,ArtisticUser和User是两种不同的类型。且不能替换使用。
package main import ( "fmt" ) func main() { user := &User{name: "Chris"} artisticUser := &ArtisticUser{User: user} fmt.Printf("user's type is: %T\n", user) fmt.Printf("artisticUser's type is: %T\n", artisticUser) acceptUser(user) //acceptUser(artisticUser) } type User struct { name string } func (u *User) sayHi() { u.sayName() u.sayType() } func (u *User) sayName() { fmt.Printf("I am %s.", u.name) } func (u *User) sayType() { fmt.Println("I am a user.") } type ArtisticUser struct { *User } func (a *ArtisticUser) sayType() { fmt.Println("I am an artistic user.") } func acceptUser(u *User) { }
user's type is: *main.User artisticUser's type is: *main.ArtisticUser
如果你尝试去掉注释掉的那一行,你会得到一个build错误:
cannot use artisticUser (type *ArtisticUser) as type *User in argument to acceptUser
要我说,嵌入类型既不是继承,也不是组合,只是跟它们都有点像。
多态性
那么回到我的问题。事实上我一开始就不该尝试继承。即使Go提供了继承机制,覆盖一个父类的非抽象方法也将破坏里氏替换原则。我一开始想要试试继承其实是一种偷懒的行为,因为我并不想重构已有的那么一大坨代码。
但是我们不应该害怕重构。你看,就算我想试着逃避重构,还是掉进别的沟里了。
如果能重来,我要选李白。。。呸,如果能让我重构已有代码的话,也许我可以试试接口。在Go语言中,接口非常灵活,是实现多态的手段。
package main import ( "fmt" ) func main() { user := &User{name: "Chris"} user.ISubUser = &NormalUser{} user.sayHi() user.ISubUser = &ArtisticUser{} user.sayHi() } type ISubUser interface { sayType() } type User struct { name string ISubUser } func (u *User) sayHi() { u.sayName() u.sayType() } func (u *User) sayName() { fmt.Printf("I am %s.", u.name) } type NormalUser struct { } func (n *NormalUser) sayType() { fmt.Println("I am a normal user.") } type ArtisticUser struct { } func (a *ArtisticUser) sayType() { fmt.Println("I am an artistic user.") }
I am Chris.I am a normal user. I am Chris.I am a artistic user.
这样我就重用了sayName和sayHi方法,并且把sayType方法留给多态来实现。
完美。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Sentinel Dashboard中修改规则同步到Nacos
上一篇我们介绍了如何通过改造Sentinel Dashboard来实现修改规则之后自动同步到Apollo。下面通过这篇,详细介绍当使用Nacos作为配置中心之后,如何实现Sentinel Dashboard中修改规则同步到Nacos。关于下面改造的原理和分析可以见上一篇《Sentinel Dashboard中修改规则同步到Apollo》的头两节内容,这里不重复介绍了。 代码实现 下面直接来看看如何实现的具体改造步骤,这里参考了Sentinel Dashboard源码中关于Nacos实现的测试用例。但是由于考虑到与Spring Cloud Alibaba的结合使用,略作修改。 第一步:修改pom.xml中的sentinel-datasource-nacos的依赖,将<scope>test</scope>注释掉,这样才能在主程序中使用。 <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</ar...
- 下一篇
微服务注册中心注册表与hashcode实现golang版
背景 基于负载均衡的服务调用 基于负载均衡的服务相互调用指的是通过基于Lvs、Haproxy、Nginx等负载均衡软件来构建一个负载均衡服务,所有的服务调用都通过负载均衡器 从负载均衡的这种模式下其实有两个主要的问题: 一是中心化,整个系统都基于负载均衡器,负载均衡就相当于整个业务的中心,虽然我们可以通过一些高可用手段来保证,但其实内部流量通常是巨大的,很容易出现性能瓶颈 二是增加了一次TCP交互 当然也有很多好处,比如可以做一些负载均衡、长链接维护、分布式跟踪等,这不是本文重点 基于注册中心的服务调用 所有的服务都启动后都通过注册中心来注册自己,同时把注册中心里面的服务信息拉回本地,后续调用,就直接检查本地的服务和节点信息来进行服务节点的调用 注册中心中的注册表 每个服务节点都会来注册中心进行服务注册,那数据如何在服务端进行保存呢,其实就是注册表,其实等同于windows 里面的注册表,每个服务都来注册,把自己的信息上报上来,然后注册中心吧注册表,返回给client端,那服务之间就知道要调用服务的节点啦 注册中心事件队列 微服务注册注册中心通常会大量的服务注册, 那不能每次客户端来请...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Red5直播服务器,属于Java语言的直播服务器
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS8安装Docker,最新的服务器搭配容器使用
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程