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方法留给多态来实现。
完美。
- 代码重用(继承和组合)
- 两种代码重用机制——组合与继承
- 二十四、继承(一) 代码重用、继承方式、接口继承与实现继承、继承和重定义、继承和组合
- java代码-------继承的方法----重写还是重载
- 组合还是继承,这是一个问题?——由模式谈面向对象的原则之多用组合、少用继承
- 复用类——组合,代理,还是继承?
- 第十九章 19 利用私有继承来实现代码重用
- 使用了继承、多态还有工厂模式和反射,但是还是没有OO的感觉。[已经增加了实现的代码]
- 编写高质量代码改善C#程序的157个建议——建议103:区分组合和继承的应用场合
- C++学习笔记(第14章->代码重用->包含,继承,多重继承,虚基类)
- 组合还是继承,这是一个问题?——由模式谈面向对象的原则之多用组合、少用继承
- 挂多个class还是新建class——多用组合,少用继承
- 挂多个class还是新建class —— 多用组合,少用继承
- 面向对象:类组合还是类继承?
- 《C++编程思想》 第十三章 继承和组合 (原书代码+习题+解答)
- (总结)C++中实现代码重用的手段----继承和聚合
- 【转】代码复用原则:合理使用类组合和类继承
- (总结)C++中实现代码重用的手段----继承和聚合
- Go语言的接口interface、struct和组合、继承
- 用继承,组合复用代码及default的继承范围