聊聊Kotlin中的继承和组合

在众多介绍软件架构的书籍中,特别是 OOP 语言为基准的话,你一定听说过一句话“组合优于继承” 【Favor composition over inheritance】

为什么这么说? 我们来看一个例子。继承非常好理解,我们需要创建一只鸡,一只鸭,我们很容易抽象出“鸟类”这个父类。如下:

open class Bird {
    val haveWing = true
    fun fly() {
        println("I can fly")
    }
}

class Duck : Bird() {
    fun swim() {
        println("I can swim")
    }
}

class Chicken : Bird() {
    fun whoop() {
        println("go go go")
    }
}

同时兼顾了 鸭子会游泳,鸡会打鸣这种特性。非常简单好理解。

但现实情况(需求)总是在改变。某一天,系统中出现了一不会飞的鸟- 鸵鸟,虽然也是鸟类,有翅膀,但是并不会飞,因此将飞行的动作下放到具体的子类中去,这时候需要修改所有现有的子类了。

open class Bird {
    val haveWing = true
}

class Duck : Bird() {
    fun swim() {
        println("I can swim")
    }
    fun fly() {
        println("I can fly")
    }
}

class Chicken : Bird() {
    fun whoop() {
        println("go go go")
    }
    fun fly() {
        println("I can fly")
    }
}
class Ostrich : Bird() {
//我不会飞
}

虽然功能是 OK 了,这样如果后续需要对 fly 进行一些调整的话,所有具体类的fly 都要调整,对于一些历史代码或者library,根本无法随便更改。再举个例子,系统中某天引入了狗,这只狗也会whoop,我们新增一个 Dog类。 Animal作为所有子类的共同父类,具备如下继承关系。

open class Animal{

}
open class Bird : Animal() {
    val haveWing = true
}

class Duck : Bird() {
    fun swim() {
        println("I can swim")
    }

    fun fly() {
        println("I can fly")
    }
}

class Chicken : Bird() {
    fun whoop() {
        println("go go go")
    }

    fun fly() {
        println("I can fly")
    }
}

class Dog : Animal() {
    fun whoop() {
        println("wang wang wang")
    }
}

我们发现,whoop 没办法放到 chicken 和 dog 的共同父类 Animal 中去实现复用,因为 Duck 等其他鸟类和动物并不能处理这种行为。而很多 oop 编程语言中,比如 Java 和 C# 中也不支持多继承。 因此,要想实现复用逻辑,我们就要使用组合模式 composition.

首先,我们定义飞行接口 IFly, 同时写一个实现(也就是我们要将duck 和 chicken 的fly 行为的具体实现抽离到一个地方来实现,以便复用代码,一次更改,全部生效)。

interface IFly {
    fun fly()
}
class FlyBehaviour : IFly {
    override fun fly() {
        println("I can fly")
    }
}

然后让duck 和 chicken 具备这种行为,首先可以让它们都实现 IFly 接口,当然自己不能真正处理,交给上面的实现来真正处理,修改如下。

class Duck(val flyBehaviour: IFly) : Bird(),IFly {
    fun swim() {
        println("I can swim")
    }

    override fun fly() {
        flyBehaviour.fly()
    }
}

class Chicken(val flyBehaviour: IFly) : Bird(), IFly {
    fun whoop() {
        println("go go go")
    }

    override fun fly() {
        flyBehaviour.fly()
    }
}

这样我们可以让两者从 is-A (继承)变为了has-A(组合,拥有),我们在运行时,可以通过传参数或者其他 依赖注入来实例化具体的实现。全部代码如下

fun main() {
    val dog= Dog()
    dog.whoop()
    val flyBehaviour=FlyBehaviour()
    val duck = Duck(flyBehaviour)
    duck.fly()
    duck.swim()
    val chicken= Chicken(flyBehaviour)
    chicken.fly()
    chicken.whoop()
}
interface IFly {
    fun fly()
}
open class Animal{

}
open class Bird : Animal() {
    val haveWing = true
}

class Duck(val flyBehaviour: IFly) : Bird(),IFly {
    fun swim() {
        println("I can swim")
    }

    override fun fly() {
        flyBehaviour.fly()
    }
}

class Chicken(val flyBehaviour: IFly) : Bird(), IFly {
    fun whoop() {
        println("go go go")
    }

    override fun fly() {
        flyBehaviour.fly()
    }
}
class FlyBehaviour : IFly {
    override fun fly() {
        println("I can fly")
    }
}
class Dog : Animal() {
    fun whoop() {
        println("wang wang wang")
    }
}

这样我们需要修改实现,可以只修改FlyBehaviour的实现内容。同时如果需要的话,也可以为 Chicken 和 Duck传入不同的flyBehaviour

但是我们还是发现,这样上面的样板代码还是有点多。比如 Chicken 和 Duck 需要实现 IFly接口的每一个方法,然后再调用 flyBehaviour的具体实现,如果 IFly 有很多通用能力的话,就非常啰嗦。好在 Kotlin 给我们提供了一个代理语法通过 by 我们可以一行代码不用写 直接将需要的能力代理过去,最终修改后的 Chicken 和 Duck 如下

class Duck(val flyBehaviour: IFly) : Bird(), IFly by flyBehaviour {
    fun swim() {
        println("I can swim")
    }
}

class Chicken(val flyBehaviour: IFly) : Bird(), IFly by flyBehaviour {
    fun whoop() {
        println("go go go")
    }
}

是不是看上去,像是又继承又组合的风格? 可拓展性也大大提高了。但是这里需要注意的是,继承,在父类的实现中可以调用 子类的方法,组合的实现则无法直接访问到对应owner的具体方法。 我们假设在 fly 的实现过程中,一些鸟内需要助跑一段时间。则飞行接口 IFly中可以再声明一个 run 方法。那么在 fly 方法中调用,不会调用到具体被代理者的 run 方法中,代码展示如下

fun main() {
    val flyBehaviour = FlyBehaviour()
    val duck= Duck(flyBehaviour)
    duck.fly()
}
interface IFly {
    fun fly()
    fun run()
}

class Duck(val flyBehaviour: IFly) : Bird(), IFly by flyBehaviour {
    fun swim() {
        println("I can swim")
    }

    override fun run() {
        println("duck is running")//这里无法通过 调用Duck的 fly 来 执行 run
    }
}
class FlyBehaviour : IFly {
    override fun fly() {
        run()//起飞前先助跑一段
        println("I can fly")
    }

    override fun run() {
        println("call in FlyBehaviour ")//fly调用的是这个方法
    }
}

open class Animal{

}
open class Bird : Animal() {
    val haveWing = true
}

输出如下

最后,Favor composition over inheritance 并不是铁律,通过继承,我们可以很直观的复用代码。通过组合我们需要构造更多对象以及维护这些对象,增加了复杂度,同时还有样板代码(虽然 Kotlin 代理帮我们减少了很多)。 因此,使用继承还是组合模式,应该要充分考虑实际场景,这看起来像一句废话,说点更具体的,比如上面例子我将 fly 理所当然的当做了 bird 的能力,实际上是考虑不周,以这个为例子,如果你十分笃定子类一定具备这样的能力,才考虑继承关系。

Leave a Reply

Your email address will not be published. Required fields are marked *