多用组合少用继承
我们再来讲讲另外一个原则:组合优于继承,多用组合少用继承。
为什么这么说呢?
1.为什么不推荐使用继承?
由于继承层次过深、过复杂,很多人觉得继承是一种反模式,应该尽量少用,甚至不用。为什么会有这样的争议?我们举个例子来解释一下。
假设我们要设计一个关于鸟的类,我们定义了一个抽象类AbstractBird,比如麻雀、鸽子、乌鸦等,都继承这个抽象类。
大部分鸟都会飞,所以我们定义了一个fly()方法,但是凡事有例外,比如鸵鸟是不会飞的,所以我们让鸵鸟类重写了fly()方法,让它抛出UnSupportedMethodException异常:
|
|
这种设计思路虽然可以解决问题,但是不够优美。因为还有很多其他鸟也不会飞,这样写增加了代码的维护量,也不符合最小知识原则。
所以我们提出另外一种方案,我们把AbstractBird派生出另外两个细分的抽象类:会飞的鸟类 AbstractFlyableBird 和不会飞的鸟类 AbstractUnFlyableBird,继承关系如下图所示:
整体来讲,目前的继承关系还比较简洁,层次也不深,可以接受。但是实际情况中随着业务的深入,我们的实现会越来越复杂。比如说现在又增加了”鸟会不会叫“这个关注点,这时候我们又该如何设计呢?
按照刚才的设计思路,我们需要定义四个抽象类,如果再考虑”鸟会不会下蛋“,那这个抽象类的个数是指数级上升的。
类的继承层次越深,继承关系越复杂,代码的可读性就会越差。子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改就会影响所有子类的逻辑。
2.组合相比继承有哪些优势?
实际上,我们可以使用**组合(composition)、接口(interface)、委托(delegation)**三个技术手段,来解决刚刚继承存在的问题。
我们知道接口具有行为特性,针对”会飞“这样一个行为特性,我们定义一个Flyable接口,所有会飞的鸟都实现这个接口,对于会叫、下蛋这些行为,也是如此。
|
|
但是接口只声明方法,不定义实现,也就是说每个会下蛋的鸟都要事先下蛋的方法。这样就会导致代码重复问题,那么这个问题又该如何解决呢??
我们针对这三个接口,再定义三个实现类,这三个实现类分别实现了fly()、tweet()和layEgg()方法,然后在具体的”鸟“对象中委托这三个实现类来实现相应的功能,通过组合和委托技术来消除代码重复,伪代码如下:
|
|
小结
我们知道继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。而这三个作用都可以通过其他技术手段来达成。比如 is-a 关系,我们可以通过组合和接口的 has-a 关系来替代;多态特性我们可以利用接口来实现;代码复用我们可以通过组合和委托来实现。所以,从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目中不用或者少用继承关系,特别是一些复杂的继承关系。
3.如何判断该用组合还是继承?
尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。
基本原则
如果类之间的继承结构稳定,继承层次较浅。继承关系不复杂,我们可以大胆地使用继承;反之则尽量用组合来替代继承。
设计模式中的固定用法
除此以外,还有一些设计模式会固定使用继承或者组合,比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。
其他特殊场景
有的时候,A和B有共同的代码但是并不具有继承关系(既不是父子也不是兄弟),这时候应该是用组合更加合理;
如果你不能改变一个函数的入参类型,而入参又非接口,为了支持多态,只能采用继承来实现。比如下面这样一段代码,其中 FeignClient 是一个外部类,我们没有权限去修改这部分代码,但是我们希望能重写这个类在运行时执行的 encode() 函数。这个时候,我们只能采用继承来实现了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
public class FeignClient { // Feign Client框架代码 //...省略其他代码... public void encode(String url) { //... } } public void demofunction(FeignClient feignClient) { //... feignClient.encode(url); //... } public class CustomizedFeignClient extends FeignClient { @Override public void encode(String url) { //...重写encode的实现...} } // 调用 FeignClient client = new CustomizedFeignClient(); demofunction(client);