委托 (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~