清水泥沙

golang基础(30.通过组合实现类的继承和方法重写)

GO不想其他类型的语言通过extends关键字来显式定义子类和父类的关系,而是通过组合的方式来实现类似的功能。显式定义继承关系的弊端有两个:一个是导致类的层级复杂,另一个是影响了类的扩展性,设计模式里面推荐的也是通过组合来替代继承提高类的扩展性。我们来看一个例子,现在有一个父类 Animal,有一个属性 name 用于表示名称,和三个成员方法,分别用来获取动物叫声、喜欢的食物和动物的名称:

package main

import "fmt"

type Animal struct {
	name string
}

func (a Animal) Call() string {
	return "动物的叫声"
}

func (a Animal) FavorFood() string {
	return "爱吃的食物"
}

func (a Animal) GetName() string {
	return a.name
}

type Dog struct {
	Animal
}

func (d Dog) FavorFood() string {
	return "骨头"
}

func (d Dog) Call() string {
	return "汪汪汪"
}

func main() {
	animal := Animal{"狗"}
	dog := Dog{animal}
	fmt.Println(dog.GetName(), "叫声:", dog.Call(), "喜爱的食物:", dog.FavorFood())
}

对应的打印结果如下:

狗 叫声: 汪汪汪 喜爱的食物: 骨头

与其他语言机制不同的是,这种组合方式更加灵活。不用考虑单继承或者多继承,你想服用哪个类的方法,直接组合进来就好了。(需要注意组合的类型中包含同名方法,如果子类没有重写,调用的时候会报错),另外,我们可以通过任意调整被组合类型位置改变类的内存布局:

type Puppy struct {
	Animal
	Dog
}
type Puppy struct {
   Dog
   Animal
}

虽然功能一致,但是内存结构不同。需要注意的是,这种情况下,如果两个类型包含同名方法和属性并且 Puppy 中没有定义这些属性或重写对应的方法,直接在 Puppy 实例上调用的话,会因冲突而报错。另外,在 Go 语言中,你还可以以指针方式继承某个类型的属性和方法:

type Dog struct { 
    *Animal
}

这种情况下,除了传入 animal 实例的时候要传入指针引用之外,其它调用无需修改:

animal := Animal{"狗"}
dog := Dog{&animal}
fmt.Println(dog.GetName(), "叫声:", dog.Call(), "喜爱的食物:", dog.FavorFood())

结构体是值类型,如果传入值字面量的话,实际上传入的是结构体值的副本,对内存耗费更大,所以传入指针性能更好。最后,Go 语言没有类似 PHP 的 parent 关键字,我们可以把组合进来的类型当做子类的一个匿名字段,直接通过引用类型名调用父类被重写的方法或属性:

fmt.Println(dog.Animal.name, "叫声:", dog.Animal.Call(), "喜爱的食物:", dog.Animal.FavorFood())

也可以将其作为一个类型为其指定一个属性名称来调用对应的方法和属性:

type Dog struct {
    name string
    animal *Animal
}

...

fmt.Println(dog.animal.name, "叫声:", dog.animal.Call(), "喜爱的食物:", dog.animal.FavorFood())