Kotlin 委托详解,从第一性原理讲解 Kotlin 委托

委托 (aka 类委托)

​ 今天在看kotlin的相关文档,读到了关于委托的相关知识点。kotlin是原生支持委托的,在文档中直接说明了“经过证明,委托模式是继承的一种好的替代实现,kotlin原生支持委托模式以消除样板式代码”。不由产生几个疑问:

1.什么是委托模式,它和代理模式有何区别?

2.为什么要用委托模式代替继承?

3.样板式代码指的是什么?

​ 在查询了相关的文献后,发现其实这块设计到了许多代码设计上的问题。既然要用委托模式代替继承,那么自然要说说继承的劣势了。

​ 从kotlin/java语言本身出发,它们只支持单继承。所以我在继承一个类后,就不能再继承其它类了,限制了类的扩展。那么为什么不支持多继承,多继承有什么坏处?在传统OOP编程中(例如C++)是支持多继承的,经过前人论证总结结合我的一些理解,总结了多继承有以下坏处:

多继承的坏味道

1.歧义

​ 若一个父类A有两个子类B,C。此时有类D同时继承了B和C,假设A类中有某个方法method(),B和C都重写了此方法。此时调用D类中的method(),就会产生歧义:不知道到底是调用B还是调用C中的method()。这就是著名的钻石问题(或是棱形问题)。

​ Java在JDK1.8之前是不存在钻石问题的。Java8的接口默认方法允许在接口中使用default声明默认实现,这样又导致了B,C(这里是接口)同时实现method(),从而产生混淆。因此为了解决该问题,编译器在编译时会检查D所继承的B,C中,是否同时实现了method(),若只有一个接口实现了method()则不会产生歧义,编译通过;若都实现了method()则编译失败(IDE也会进行检查),必须在D类中重写method()以指明该调用哪个父接口中的method()「super.A.method()」或者只是重写method()。举例:

interface A {
    void method();
}

interface B extends A {
    @Override
    default void method() {

    }
}

interface C extends A {
    @Override
    default void method() {

    }
}

class D implements B, C {

    @Override
    public void method() {
        B.super.method();
    }
}

​ 那么既然都会导致钻石问题(即使在1.8之前,这也不是使用接口的核心原因),那为什么还要使用接口呢?所以引出了第二点问题

2.逻辑不合理

​ 汽车对象Car,它有轮胎(Tyre),发动机(Engine),变速箱(Transmission)等等,Car可以是FancyCar,BustCar。现在有一辆布加迪,如果用多继承的话,表现为(伪代码):

class Bugatti extends FancyCar, Tyre, Engine, Transmission {}

​ 表面上看上去是没问题的,但是仔细想一下。继承更多的表现了 IS-A 也就是「是一个」 这个关系上,布加迪是一辆FancyCar没错,但你能说它是一个发动机吗?发动机与布加迪的关系是 HAS-A,也就是「有一个」,我们可以说布加迪有一个发动机。所以,最好的方式是使用组合:

class Bugatti extends FancyCar {
  Tyre tyre;
  Engine engine;
  Transmission transmission;
}

​ 现在逻辑上更加清晰合理,不再「黏糊糊」了,是的,绝大多数多继承问题都可以采用此方式解决。但是,我们在面向调用者的时候,这种方式显得又些笨拙,比如我想启动一辆汽车引擎,调用者不想关心你用的是组合还是继承,我只希望通过Car对象能直接启动引擎。如果是使用组合,可能要这样写:

void startEngine(Bugatti bugatti) {
  	bugatti.getEngine().start();
}

​ 我现在想要实现下面的效果:

void startEngine(Bugatti bugatti) {
  	bugatti.start();
}

​ 此时,实现多接口就起到了作用。**接口意味着「正在做什么」或者「有什么样的能力(功能)」,继承更像是「我该如何做」。**例如Serializable接口意味着类支持序列化,Cloneable接口表示类可以克隆。此时,我想让布加迪具备直接启动引擎的能力,但是具体实现交给Engine对象去做:

class Bugatti extends FancyCar implements Engine {
  Engine engine;
  
  @Override
  public void start() {
    engine.start();
  }
}

​ 看,我们做到了。现在布加迪是一辆FancyCar,同时具备了引擎的功能,引擎的具体实现是交给Engine对象做的。这种实现方式,就是大名鼎鼎的「委托模式」或「代理模式」

​ 但是我们也看到了一些缺陷,在Bugatti类中,我们重写了start()但没有做任何额外处理。委托模式的大多数情况下都是这种样板代码,java中无法避免这种情况,于是kotlin原生支持了委托(java 可以采用 lombok 的 @Delegate,同样可以消除样板代码)

kotlin 委托

在kotlin中,对于类的委托我们只需要这样写:

class Bugatti(
    tyre: Tyre,
    engine: Engine,
    transmission: Transmission
) :
    FancyCar,
    Tyre by tyre,
    Engine by engine,
    Transmission by transmission

Bugatti 的实现交给传入的三个对象做,完美解决样板代码问题。

多继承完全不合理吗

​ 答案是否定的,多继承在某些场景下是合理的,比如玫瑰花既是植物也是装饰品,但是装饰品不一定是植物,装饰品和植物没有继承关系,这个时候多继承就是合理的。在单继承的语言中,如果想要对外提供一致的功能的话,就只能使用实现(多)接口来做(此时可以用委托模式以消除这种方法论下的模版代码),唯一继承的对象要选择尽可能直观且更具相关性的对象,比如此处的玫瑰花就可以继承植物对象实现装饰品接口。若不需要提供一致的功能,则可以使用组合来减少类似于java中的样板代码。

三个问题的解答

​ 关于「什么是委托模式,它和代理模式有何区别?」这个问题,有些书上说代理模式又叫委托模式。我认为还是有些差别的:代理模式通常指设计模式中的代理模式,代理模式是委托模式的一种实现,上文演示的其实就是代理模式。所以代理模式更多意味着类的代理,它是一种结构型模式。但是委托模式不仅可以是类的代理,也可以是属性的代理,例如kotlin中的属性委托。所以,委托模式是代理模式的超集,应该从继承结构角度划分。另外关于「委托模式」、「委托」、「委派」,我认为是同一个意思。

​ 问题2「为什么要用委托模式代替继承?」,现在可以解答了。主要是(暂不讨论属性委托):kotlin类只能单继承,为了实现多继承(IS-A)或者为了体现 HAS-A 的思想,我们采用接口实现,接口实现则导致大量样板代码,委托模式可以消除样板代码。简而言之,就是为了消除「实现多个接口方法时,写大量样板代码」这个问题

​ 问题3 「样板式代码指的是什么?」,是指在java语言中,由于没有委托语法,只能重实现方法并调用父类方法且不做任何额外处理的代码。

属性委托

later~

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值