美文网首页
Go 组合与继承那些事

Go 组合与继承那些事

作者: Java编程日记 | 来源:发表于2022-04-23 20:10 被阅读0次

Go 不是一个(传统的)面向对象语言,尽管通过各种奇技淫巧可以实现 OO 的编程风格。

我不赞成「如何在 A 实现 B」之类的尝试。每个东西都有它自己的特点,这个特点用好了就是优点,用不好就是缺点。非要用汽车拉磨或用驴子拉货,何必呢。

继承 vs 组合

一句话解释,继承是「is sth」,组合是「has sth」。Go 采用组合完美契合了它 鸭子类型(duck typing) 的设计理念。

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

鸭子类型中,我们重点关注对象能做什么,而不在意它究竟是什么。

对这个理念我略有感触。曾经在 Kotlin (java) 开发中遇到过这样的问题:

第三方包中有个类,没有抽象出接口,我恰恰需要扩展这个东西。于是只好自己定义一个接口,然后写个代理类或者用其他奇奇怪怪的方法达成目的。

你看,它明明是我接口的实现,仅仅因为缺少 implements 关键字,我就得大费周章。

在鸭子类型中这个问题不复存在。

组合要比继承灵活得多。比如 java 中不能让「卡车」既继承「车」又继承「货运工具」,这又偏偏是显示情况。你不能建模为「车 <- 货运工具 <- 卡车」,因为货运工具也可能是飞机。而组合可以轻松办到:

<pre class="prettyprint hljs elm" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">type Car struct { Id string }
type Plane struct { Owner string }
type Logistics struct { CargoType string }

type Truck struct {
Car
Logistics
}

type An255 struct {
Plane
Logistics
}
</pre>

组合绝非继承

本质是语法糖

一些博客会把下面两种写法等价:

<pre class="prettyprint hljs scala" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">// java
public class Animal {
public String name;
}

public class Dog extends Animal { }
</pre>

<pre class="prettyprint hljs rust" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">// Go
type Animal struct {
name string
}

type Dog struct {
Animal
}
</pre>

它们用起来确实很类似,都可以通过“子类”直接访问“父类”的属性 Dog.name ,但这两个有本质差别:

对于 java, name 确实是 Dog 的属性,不可以 Dog.Animal.name 这样来访问。可对于 Go, Dog 是没有 name 属性的。 **Dog.name 只是一个 Dog.Animal.name 的语法糖。 **实际上 Dog 中有一个类型为 Animal 的变量(默认变量名与类型一致), name 依然只属于 Animal 。为了更加明显,我们可以给这个变量指定名字:

<pre class="prettyprint hljs go" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">type Dog struct {
innerVar Animal
}

dog := Dog{Animal{"D"}}
println(dog.innerVar.Name)
</pre>

对象只有一个类型

有人要说了,管它本质是啥,能用不就完事了么。可惜,你用不了… 来看看下面一种典型错误:

<pre class="prettyprint hljs cpp" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">// java
public void feed(Animal a) { }
feed(new Dog()); // ok
</pre>

<pre class="prettyprint hljs go" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">// Go
func feed(a Animal) { }
feed(Dog{Animal{"D"}}) // error
</pre>

在 java 中,因为有继承, Dog 也是一个 Animal ,因此这么传参毫无问题。不过在 Go 中,对象只能有一个类型——是 Dog 了就不能是 AnimalDog 的确包含 Animal 但它还是 dog。就好像,汽车包含轮子,它还叫汽车,不能管它叫轮子。

建模思路

道理我都懂,可还是觉得奇怪

那是因为我们的命名太有误导性,或者说,建模思路就错了。我们随手就能写出

<pre class="prettyprint hljs scala" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">// java
public class Dog extends Animal { }
</pre>

这样的例子,我想没人会这么写:

<pre class="prettyprint hljs scala" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">// java
public class Car extends Wheel { } // Wheel 是车轮
</pre>

很显然,仅管「轮子」比「车」更底层,但它们没有继承关系。

而在 Go 中,用「组合」的思想,把后者实现一遍:

<pre class="prettyprint hljs rust" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">type Wheel struct { }
type Car struct {
Wheel
}
</pre>

诶,「车拥有轮子」,是不是通顺多了 既然 java 中行不通的思路在 Go 里毫无违和感,那反过来,把 java 里的常规思路按照所谓的“等价写法”放在 Go 里呢?「猫拥有动物」「货车拥有车」???

由此可见,Go 的设计与传统面向对象完全不同。我们也不能把之前的 OOP 思路强行套在 Go 的开发中。更不应该去找什么「等价写法」。

Go 是组合而非继承,因此在建模过程中我们得 摒弃层级观念,把线性结构转为换网状结构。 比如 人 <- 教师 <- 地理教师 可以转换为 地理教师 consist of(人,地理,教师)

参数传递

建模完毕,使用中少不了传参。Go 没有继承,自然也就不能「定义父类型形参,传子类型对象」了。解决办法有两种。

直接传“子类型”

最粗暴的方案。

<pre class="prettyprint hljs go" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">type AnimalBaseInfo struct { Name string }
type Dog struct { AnimalBaseInfo }

func feed(a AnimalBaseInfo) { // “父类”形参
println("Feed" + a.Name)
}

func main() {
dog := Dog{AnimalBaseInfo{"D"}}
feed(dog.AnimalBaseInfo) // 直接传“子类”对象
}
</pre>

缺点是会丢失额外信息。 feed() 无法恢复 aDog 做进一步处理。

定义接口

这个需求正是接口要做的。

<pre class="prettyprint hljs go" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">type Animal interface { Name() string }
type AnimalBaseInfo struct { name string }
type Dog struct { AnimalBaseInfo }

// Dog 实现接口,此时可以说,Dog 是 Animal
func (d *Dog) Name() string {
return d.name
}

func feed(a Animal) {
println("Feed" + a.Name())
}

func main() {
dog := Dog{AnimalBaseInfo{"D"}}
feed(&dog) // 传 dog 自己
}
</pre>

如果 AnimalBaseInfo 字段较多,实现接口是需要写很多方法,那么可以把它们用一个 struct 表示:

<pre class="prettyprint hljs go" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">type Animal interface { Info() AnimalBaseInfo } // 接口直接返回结构体

type AnimalBaseInfo struct {
name string
age int
sex bool
}
type Dog struct { AnimalBaseInfo }
func (d *Dog) Info() AnimalBaseInfo { // 接口实现
return d.AnimalBaseInfo
}

func feed(a Animal) {
println("Feed" + a.Info().name)
}</pre>

相关文章

  • Go 组合与继承那些事

    Go 不是一个(传统的)面向对象语言,尽管通过各种奇技淫巧可以实现 OO 的编程风格。 我不赞成「如何在 A 实现...

  • <>

    结构体及其使用方法 结构体的定义 Go语言用嵌入式字段实现了继承吗?? GO语言不存在所谓的继承,只有组合。组合和...

  • 27. 组合取代继承

    27. 组合取代继承 Go 不支持继承,但它支持组合(Composition)。组合一般定义为“合并在一起”。汽车...

  • PHP学习2

    六.继承与多态 1. 类的组合和继承(继承===“是、像”、“父与子”,组合===“需要”、“整体与局部”) 组合...

  • Go中如何继承

    [TOC]在这里简单分享一下在Go中如何实现继承。 1. 简单的组合 说到继承我们都知道,在Go中没有extend...

  • js一些技巧.md

    js中的constructor和prototype 组合继承与寄生继承 组合继承 可以继承实例属性和方法,也可以继...

  • golang-面向对象

    思考:go语言不支持继承与多态的优点与缺点是什么 go语言只支持封装,不支持继承与多态 go语言没有class,只...

  • Go教程第二十三篇: Go中的面向对象:组合而不是继承

    本文是《Go系列教程》的第二十三篇文章。 Go本身不支持继承,但是它支持组合。组合的通常定义是:“放置在一起”。组...

  • Golang-TCP异步框架Tao分析

    TCP异步框架 Golang 编程风格 Go语言面向对象编程的风格是多用组合,少用继承,以匿名嵌入的方式实现继承。...

  • 组合与继承

    原文链接http://zhhll.icu/2020/05/06/java%E5%9F%BA%E7%A1%80/%E...

网友评论

      本文标题:Go 组合与继承那些事

      本文链接:https://www.haomeiwen.com/subject/uqcoertx.html