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

代码重用

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

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. linux批量解压和批量压缩

    ls *.tar.gz | xargs -n1 tar xzvf //批量解压 ls | awk '{ print "tar zcvf "$0".tar.gz " ...

  2. 【转】fedora和ubuntu开启ssh

    fedora和ubuntu开启ssh 1小时前 ubuntu开启SSH服务 SSH分客户端openssh-client和openssh-server如果你只是想登陆别的机器的SSH只需要安装opens ...

  3. 创建git仓库及简单操作命令

    1.把已有的项目代码纳入git管理 $ cd projectdir  #projectdir项目代码所在的文件夹 $ git init 2.新建的项目直接使用git管理 $ cd dir  #dir ...

  4. [Javascript] Replicate JavaScript Constructor Inheritance with Simple Objects (OLOO)

    Do you get lost when working with functions and the new keyword? Prototypal inheritance can be compl ...

  5. Intel Edision —— 开发环境选择一贴通

    前言 原创文章,转载引用务必注明链接.如有疏漏,欢迎斧正. 使用Intel开发板设置工具配置好之后,会自动跳转到集成开发环境(integrated development environment,ID ...

  6. Dynamics CRM 2015中的SSRS Report集成配置

    大家应该都知道.Dynamics CRM能集成SSRS Report,而且我也在之前的博文中讨论过怎样制作一个简单的SSRS Report并部署到Dynamics CRM中.今天我们来看看一些比較有用 ...

  7. github 新建远程仓库 及 删除远程仓库

    一.新建远程仓库 1.点击 ' + ' 号 2.选择 ' new repository ' 3.填写信息  创建仓库 二.删除远程仓库 1.点击 ' Settings ' 按钮 2.滑动到最底部,点击 ...

  8. DataGuard备库ORA-01196故障恢复一则

    问题现象 在使用shutdown abort停DataGuard备库后.备库不能open,报ORA-01196错误. 详细例如以下: 发现一备库不能应用日志.查看备库日志没发现报错.怀疑是备库应用日志 ...

  9. Hdu 4274 Spy&#39;s Work

    Spy's Work Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total ...

  10. 李洪强经典面试案例33-如何面试 iOS 工程师

    如何面试 iOS 工程师   推荐序 私下和很多朋友交流过这个话题,大部分求职者认为,我能做基本的 iOS 开发工作,就达到公司的要求了,殊不知公司招聘员工,更希望的是这个人能够在关键时候能够发挥一般 ...