通过组合实现类的继承和方法重写


Go 语言不是像 PHP 等传统面向编程实现那样通过 extends 关键字来显式定义子类与父类之间的继承关系,而是通过组合方式实现类似功能,显式定义继承关系的弊端有两个:一个是导致类的层级复杂,另一个是影响了类的扩展性,设计模式里面推荐的也是通过组合来替代继承提高类的扩展性。

我们来看一个例子,现在有一个父类 Animal,有一个属性 name 用于表示名称,和三个成员方法,分别用来获取动物叫声、喜欢的食物和动物的名称:

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
}

如果我们要定义一个继承自该类的子类 Dog,可以这么做:

type Dog struct {
    Animal
}

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

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

我们在 Dog 结构体中,引入了 Animal 这个类型,这样一来,我们就可以在 Dog 类中访问所有 Animal 类型包含的属性和方法(如果两个类不在同一个包中,只能访问父类中首字母大写的公共属性和方法),比如这里我们可以在 Dog 实例上直接访问 Animal 类的 GetName 方法。

还可以通过在子类中定义同名方法来覆盖父类的实现,比如这里的 Call 方法和 FavorFood 方法,当我们在 Dog 实例上调用 Call 方法或 FavorFood 方法时,调用的是 Dog 类中定义的方法而不是 Animal 中定义的方法。

我们可以这样实例化 Dog 类型并调用相应的方法:

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

对应的打印结果如下:

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

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

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())

Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 为基本数据类型添加成员方法

>> 下一篇: 类属性和方法的可见性