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
了就不能是 Animal
。 Dog
的确包含 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()
无法恢复 a
为 Dog
做进一步处理。
定义接口
这个需求正是接口要做的。
<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>
网友评论