美文网首页
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 组合与继承那些事

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