在众多介绍软件架构的书籍中,特别是 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 的能力,实际上是考虑不周,以这个为例子,如果你十分笃定子类一定具备这样的能力,才考虑继承关系。