故事要从我在一个项目中,想要假装的专业一点而遇到的一个陷阱说起。

代码重用

在这个项目中,我们已经有了类似如下的代码:

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方法留给多态来实现。

完美。

Go语言中的代码重用 - 继承还是组合?的更多相关文章

  1. 《C++ Primer Plus》读书笔记之十二—C++中的代码重用

    第14章 C++中的代码重用 1.C++代码重用方法:公有继承.使用本身是另一个类的对象的类成员(这种方法称为包含.组合或层次化).私有或保护继承.类模板等. 2.模板特性意味着声明对象时,必须指定具 ...

  2. C++ primer plus读书笔记——第14章 C++中的代码重用

    第14章 C++中的代码重用 1. 使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现).获得接口是is-a关系的组成部分.而使用组合,类可以获得实现,但不能获得接口. ...

  3. C++进阶--代码复用 继承vs组合

    //############################################################################ /* * 代码复用: 继承 vs 组合 * ...

  4. 第16讲——C++中的代码重用

    C++的一个主要目标是促进代码重用.除了我们之前学的公有继承,我们在这一讲将介绍另一种代码重用的方法——类模板.

  5. c语言中一种典型的排列组合算法

    c语言中的全排列算法和组合数算法在实际问题中应用非常之广,但算法有许许多多,而我个人认为方法不必记太多,最好只记熟一种即可,一招鲜亦可吃遍天 全排列: #include<stdio.h> ...

  6. 《C++ Primer Plus》第14章 C++中的代码重用 学习笔记

    C++提供了集中重用代码的手段.第13章介绍的共有继承能够建立is-a关系,这样派生类可以重用基类的代码.私有继承和保护继承也使得能够重用基类的代码,单建立的是has-a关系.使用私有继承时,积累的公 ...

  7. 52. 模版和设计元素——Lotus Notes的代码重用

    不论是理论上还是实用上,代码重用都是编程的一个重要议题.可以从两个角度来讨论代码重用. 一是逻辑上代码以怎样的方式被重用.既可以通过面向对象的思想普及以来耳熟能详的继承的方式.比如先建了一个车的基类, ...

  8. Go语言中的面向对象

    前言 如果说最纯粹的面向对象语言,我觉得是Java无疑.而且Java语言的面向对象也是很直观,很容易理解的.class是基础,其他都是要写在class里的. 最近学习了Go语言,有了一些对比和思考.虽 ...

  9. OO的片段,继承与组合,继承的优点与目的,虚机制在构造函数中不工作

    摘自C++编程思想: ------------------------------ 继承与组合:接口的重用 ------------------------------- 继承和组合都允许由已存在的类 ...

随机推荐

  1. groovy入门 第05章 基本输入输出

    基本输入输出 5.1基本输出 print XXX    //同一行输出 println XXX //换行输出 输出字符串: def message ="My name is Michael& ...

  2. 细说linux IPC(三):mmap系统调用共享内存

    [版权声明:尊重原创,转载请保留出处:blog.csdn.net/shallnet 或 .../gentleliu,文章仅供学习交流,请勿用于商业用途]         前面讲到socket的进程间通 ...

  3. 【转载】FAT12格式的引导程序

    FAT12格式的引导程序 在上一篇文章中详细介绍了FAT12格式的引导扇区数据结构,详情请浏览: 地址是:http://blog.sina.com.cn/s/blog_3edcf6b80100cr08 ...

  4. solr单机多实例部署文件锁冲突解决的方法

    给出一个有问题的单机多tomcat实例引用同一个solr实例部署图. 这样的部署必定造成一个问题.启动第二个tomcat实例时,一定会报索引目录文件锁已经被占用. 最初的解决的方法是.有多少个tomc ...

  5. 设计模式学习笔记——Mediator中介者模式

    将众多对象之间的网状关系转为全部通过一个中间对象间接发生关系,此中间对象为中介者. 看图最直观: 作用不言而喻,就是降低对象之间的耦合度,乃至降低了整个系统的复杂度. 有点象代理模式,更象外观模式:

  6. DRF之视图组件 三次封装

    1.为什么要进行封装 1.1 在处理表的时候,如果有几十张表都需要增删改查查时,如果每一张表都写这些方法,会让代码显得冗余,所以需要将这些方法进行封装,然后不同的表都去继承这写方法.(这是思路) 1. ...

  7. Hibernate exception

    1.a different object with the same identifier value was already associated with the session. 错误原因:在h ...

  8. Duplicate Observed Data

    在翻看<重构-改善既有代码的设计>这本经典的书,书中就介绍了一个重构方法--Duplicate Observed Data 复制被监视数据的重构方法,使用这种方法能够使界面和对数据的操作隔 ...

  9. kbmMW 5.0.1发布了(跨全平台,包括Linux,可使用Win的高性能HTTPSys传输层,等等)

    kbmMW5如期发布,作者增加了很多重磅功能,以下翻译作者的发布文件:1.支持Delphi 10.2 Tokyo,包括Linux支持(测试版)2.大量的新功能与改进3.新的智能服务(Smart ser ...

  10. codeforces 436A. Feed with Candy 解题报告

    题目链接:http://codeforces.com/contest/436/problem/A 题目意思:给出 n 颗只有两种类型:fruit 和 caramel的candies,这些candies ...