1. Java的多态
我们先来看java面向对象的多态范例
abstract class Animal {
public void doActions() {
this.breathe();
this.eat();
this.sleep();
}
public void breathe() {
System.out.println("Breathing Air!");
}
abstract public void eat();
public void sleep() {
System.out.println("Sleeping Zzz!");
}
}
class Human extends Animal {
@override
public void eat() {
System.out.println("Eating everthing!");
}
}
class Cat extends Animal {
@override
public void eat() {
System.out.println("Eating fish!");
}
@override
public void sleep() {
System.out.println("Sleeping rumble!");
}
}
class Dog extends Animal {
@override
public void eat() {
System.out.println("Eating bones!");
}
@override
public void sleep() {
System.out.println("Sleeping Woof!");
}
}
class Application {
public void main(String[] arg) {
new Human().doActions();
new Cat().doActions();
new Dog().doActions();
}
}
父类Animal成员方法doActions()调用了子类覆盖后的breathe(), eat(), sleep(). 如果子类未覆盖, 则仍会沿用父类的实现. 在写Animal代码时, 子类还没写出来, 也可能子类是另外的开发者书写的.
这是面向对象多态里已经实现的. 但golang并不是面向对象的, 它没有完整的继承关系, 组合式内嵌是没有this指针, 也无法获取外层覆盖方法的.
2. golang无法调用多态
下面是golang结构体不能调用的原因分析
package main
import "fmt"
type Animal struct {
}
func (a *Animal) doActions() {
a.breathe() // a参数无法获得子类覆盖
a.eat() // 'a' cannot obtain function override
a.sleep()
}
func (a *Animal) breathe() {
fmt.Println("Breathing Air!")
}
func (a *Animal) void eat() {
panic("unimplment")
}
func (a *Animal) sleep() {
fmt.Println("Sleeping Zzz!")
}
type Human struct {
Animal
}
//@override
func (a *Human) void eat() {
fmt.Println("Eating everthing!")
}
func (a *Human) void learn() {
fmt.Println("Learning knowledge!")
}
type Cat struct {
Animal
}
//@override
func (c *Cat) eat() {
fmt.println("Eating fish!")
}
//@override
func (c *Cat) sleep() {
fmt.Println("Sleeping rumble!")
}
type Dog struct {
Animal
}
//@override
func (d *Dog) eat() {
fmt.Println("Eating bones!")
}
//@override
func (d *Dog) sleep() {
fmt.Println("Sleeping Woof!")
}
func main() {
human := &Human{}
human.doActions()
// see override, print "Eating everthing!"
human.eat()
&Cat{}.doActions()
&Dog{}.doActions()
}
很多时候, 我们经常会遇到, 部分代码先编写调用流程, 这个时候甚至子类结构体还没定义出来.
当编写具体实现代码时, 还要求有部分代码重写.
这在面向对象里太常见了, 那么与之对应的功能golang要如何实现呢?
3. 实现方案
这边推荐两种方案: 建造器和基础函数
3.1. 建造器
package main
import "fmt"
type Animal struct {
}
func (a *Animal) doActions() {
a.breathe() // a参数无法获得子类覆盖
a.eat() // 'a' cannot obtain function override
a.sleep()
}
func (a *Animal) breathe() *Animal {
fmt.Println("Breathing Air!")
return a
}
func (a *Animal) void eat() *Animal {
panic("unimplment")
return a
}
func (a *Animal) sleep() *Animal {
fmt.Println("Sleeping Zzz!")
return a
}
type Human struct {
Animal
}
//@override
func (a *Human) void eat() *Human {
fmt.Println("Eating everthing!")
return a
}
func (a *Human) void learn() *Human {
fmt.Println("Learning knowledge!")
return a
}
type Cat struct {
Animal
}
//@override
func (c *Cat) eat() *Cat {
fmt.println("Eating fish!")
return c
}
//@override
func (c *Cat) sleep() *Cat {
fmt.Println("Sleeping rumble!")
return c
}
type Dog struct {
Animal
}
//@override
func (d *Dog) eat() *Dog {
fmt.Println("Eating bones!")
return d
}
//@override
func (d *Dog) sleep() *Dog {
fmt.Println("Sleeping Woof!")
return d
}
func main() {
human := &Human{}
human.doActions()
// see override, print "Eating everthing!"
human.eat()
&AnimalBuilder{&Cat{}}.doActions().End()
&AnimalBuilder{&Dog{}}.breathe().eat().sleep().End() // 链式调用
}
type AnimalBudiler struct {
AnimalInterface
}
func (builder *AnimalBudiler) doActions() {
builder.breathe() // 获得子类覆盖
builder.eat()
builder.sleep()
}
func (builder *AnimalBudiler) End() {
}
type AnimalInterface interface {
breathe()
eat()
sleep()
}
实现原理: 利用嵌套结构体只有外部(结构体之外)代码才产生多态(有覆盖用覆盖函数, 没覆盖用原函数)的特性, 使用AnimalBuidler构造器来组织, 内部代码的调用. 也就是说把"父类"的doActions()写在了Builder里. 凡是跨父子结构体的都要抽到Builder里.
优点是对父类, 子类分别编写没有影响, 流程清晰;
缺点是相较于原先单个父类函数调用而言, 调用处的改动比较大, 而且要不断的维护AnimalInterface接口以保证创造器内部可访问子类覆盖的方法.
建造器比较适合大型流程的调用与覆盖.
3.2. 基础函数
package main
import "fmt"
type Animal struct {
}
func (a *Animal) doActions() {
a.breathe() // a参数无法获得子类覆盖
a.eat() // 'a' cannot obtain function override
a.sleep()
}
func (a *Animal) doActionsBase(food string, sound string) {
a.breathe()
a.eatBase(food)
a.sleep(sound)
}
func (a *Animal) breathe() {
fmt.Println("Breathing Air!")
}
func (a *Animal) void eat() {
panic("unimplment")
}
func (a *Animal) void eatBase(food string) {
fmt.Println("Eating "+food+"!")
}
func (a *Animal) sleep() {
a.sleepBase("Zzz")
}
func (a *Animal) sleepBase(sound string) {
fmt.Println("Sleeping "+sound+"!")
}
type Human struct {
Animal
}
//@override
func (a *Human) void eat() {
a.eatBase("everthing")
}
func (a *Human) void learn() {
fmt.Println("Learning knowledge!")
}
type Cat struct {
Animal
}
//@override
func (c *Cat) eat() {
c.eatBase("fish")
}
//@override
func (c *Cat) sleep() {
c.sleepBase("rumble")
}
type Dog struct {
Animal
}
//@override
func (d *Dog) eat() {
d.eatBase("bones")
}
//@override
func (d *Dog) sleep() {
d.sleepBase("Woof")
}
func main() {
human := &Human{}
human.doActions()
// see override, print "Eating everthing!"
human.eat()
&Cat{}.doActions()
&Dog{}.doActions()
}
实现原理: 将父类需要被子类覆盖的函数额外多建立出一个基础函数, 将影响到实现差异的部分用参数传入, 如范例中xxxBase格式命名的函数. 然后让父类和子类传入不同的参数来得到不同的处理逻辑, 也就是说实际代码是由基础函数完成的, 原函数只是组装不同的参数值.
一般来说原函数提供对外访问, 基础函数可以通过对外接口减裁掉.
注意: 基础函数的参数应当是数值变量, 不应是函数变量. 传递函数会增加复杂度, 容易造成地狱回调, 而且这也违反了数据驱动设计原则.
优点: 最接近于面向对象的代码结构, 可读性较高, 改动量最少, 后续维护成本低;
缺点: 额外多出许多函数, 另外需要将原函数抽离出变量控制执行代码;
4. 不推荐侵入性属性
type AnimalInterface interface {
breathe()
eat()
sleep()
}
type Animal struct {
// 成员变量-记录子类实例
this AnimalInterface
// 函数变量-记录子类实现函数
eat func()
sleep func()
}
func (a *Animal) doActions() {
a.this.breathe()
a.this.eat()
a.this.sleep()
}
type Human struct {
Animal
}
type Cat struct {
Animal
}
type Dog struct {
Animal
}
/// 调用时
func main() {
human := &Human{}
// 显然这不是个良好的代码--
human.Animal.this = human
human.Animal.eat = human.eat
human.Animal.sleep = human.sleep
}
我们看到额外的成员函数变量, 或者子类的成员接口, 除了占用了结构体的空间, 还增加了初始化负担.
把内层结构体把带外层结构体当作成员来使用, 这个写法读代码上就是病句.
那么写得难看,代码量就能减少了吗?实际上并没有.
代码并不是一锤子买卖, 功能会不断增加, 当功能扩展时除了实现函数要改, 成员函数变量也要增加, 同时初始化赋值还不能少, 那就是全都要改.
题外话, java底层实现确实也是使用this函数指针保持当前实例 (类似于范例中的AnimalInterface变量), 但由编译器自动完成, 无需人工编写.
换言之, 如果`侵入性属性`是自动同步修改的行为, 代码不需要开发者维护的情况下, 也是一个很好的实现方案. 或许使用`go generate`能做得到也不一定.