C++设计模式专栏:http://t.csdnimg.cn/8Ulj3
目录
1.引言
2.为什么不推荐使用继承
3.相比继承,组合有哪些优势
4.如何决定是使用组合还是使用继承
1.引言
面向对象编程中有一条经典的设计原则:组合优于继承,也常被描述为多用组合,少用继承。为什么不推荐使用继承?相比继承,组合有哪些优势?如何决定是使用组合还是使用继承?本节围绕这3个问题详细讲解这条设计原则。
2.为什么不推荐使用继承
继承是面向对象编程的四大特性之一,用来表示类之间的is-a关系,可以解决代码复用问题。虽然继承有诸多作用,但继承层次过深、过复杂,会影响代码的可维护性。对于是否应在项目中使用继承,目前存在很多争议。很多人认为继承是一种反模式,应该尽量少用,甚至不用。为什么会有这样的争议呢?我们通过一个例子解释一下。
假设我们要设计一个关于鸟的类。我们将“鸟”这样一个抽象的事物概念定义为一个抽象类 AbstractBird。所有细分的鸟,如麻雀、鸽子和乌鸦等,都继承这个抽象类。我们知道,大部分鸟都会飞,那么可不可以在AbstractBird抽象类中定义一个fly()方法呢?答案是否定的。尽管大部分鸟都会飞,但也有特例,如鸵鸟就不会飞。鸵鸟类继承具有fly()方法的父类,那么鸵鸟就具有了“飞”这样的行为,这显然不符合我们对现实世界中事物的认识。当然,读者可能会说,在鸵鸟这个子类中重写(overide)fly()方法,让它抛出UnSupportedMethodException异常不就可以了吗?具体的代码实现如下。
public class AbstractBird{//...省略其他属性和方法...public void fly(){...}
}
public class 0strich extends AbstractBird {//轮鸟类//.省略其他属性和方法.public void fly(){throw new unsupportedMethodException("I can't fy.");}
}
虽然这种设计思路可以解决问题,但不够优雅,因为除鸵鸟以外,不会飞的鸟还有一些,如企鹅,对于所有不会飞的鸟,我们都需要重写fly()方法,并抛出异常。这样的设计,一方面,徒增编码的工作量; 另一方面,违背最少知识原则(迪米特法则),暴露不该暴露的接口给外部,增加了类使用过程中被误用的概率。
读者可能又会说,可以通过 AbstractBird类派生出两个细分的抽象类:AbstractFlyableBird(会飞的鸟类)和AbstractUnFlyableBird(不会飞的鸟类),让麻雀、乌鸦这些会飞的鸟对应的类都继承AbstractFlyableBird类,让鸵鸟、企鹅这些不会飞的鸟对应的类都继承AbstractUnFlyableBird类,如下图所示。是不是就可以解决问题了呢?
从上图中,我们可以看出,继承关系变成了3层。从整体上来讲,目前的继承关系还比较简单,层次比较浅,也算是一种可以接受的设计思路。我们继续添加需求。在上文提到的场景中,我们只关注“鸟会不会飞”,但如果我们还要关注“鸟会不会叫”,那么,这个时候,又该如何设计类之间的继承关系呢?
是否会飞和是否会叫可以产生4种组合:会飞会叫、不会飞但会叫、会飞但不会叫、不会飞不会叫。如果沿用上面的设计思路,那么需要再定义4个抽象类:AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、AbstractUnFlyableTweetableBird 和 AbstractUnFlyableUnTweetableBind。此处的继承关系如下图所示。
如果我们还需要考虑“是否会下蛋”,那么组合数量会呈指数式增长。也就是说,类的继承层次会越来越深,继承关系会越来越复杂。这种层次很深、很复杂的继承关系会导致代码的可读性变差,因为我们要弄清楚某个类包含哪些方法、属性,就必须阅读父类的代码、父类的父类的代码……一直追溯到顶层父类。另外,这破坏了类的封装特性,因为将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,二者高度耦合,一旦父类的代码被修改,那么会影响所有的子类。
总之,继承最大的问题就在于:继承层次过深、继承关系过于复杂,会影响代码的可读性和可维护性。这也是我们不推荐使用继承的原因。对于本例中继承存在的问题,我们应该如何解决呢?读者可以在下文中得到答案。
3.相比继承,组合有哪些优势
实际上,我们可以通过组合(composition)、接口和委托(delegation)3种技术手段共同解决上面继承存在的问题。
在介绍接口时,我们说过,接口表示具有某种行为特性。针对“会飞”这样一个我们可以定文一个接口Flyable,只让会飞的鸟去实现这个接口。对于会叫、会下蛋这两个行为特性,可以类似地分别定义Tweetable接口、EggLayable接口。我们将此设计思路翻译成Java 代码如下所示。
public interface Flyable {void fly();
}
public interface Tweetable {void tweet();
}
public interface EggLayable {void layEgg();
}
public class 0strich implements Tweetable,EggLayable{//轮鸟类//...省略其他属性和方法.@Overridepublic void tweet(){ ...}@Overridepublic void layEgg(){... }
}
public class Sparrow impelents Flyable, Tweetable, EggLayable {//麻雀类//...省略其他属性和方法...@Overridepublic void fly(){... }@Overridepublic void tweet(){...}@Overridepublic void layEgg(){...}
}
不过,我们知道,接口只声明方法,不定义实现。也就是说,每个会下蛋的鸟都要实现遍layEgg()方法,并且实现逻辑是一样的,这就会导致代码重复的问题。对于这个问题,我们可以以针对3个接口再定义3个实现类: 实现了fly()方法的FlyAbility类、实现了twee()方法的TweetAbility类和实现了layEgg()方法的EggLayAbility类。然后,我们通过组合和委托技法消除代码的重复问题。具体的代码实现如下。
public interface Flyable {void fly();
}
public class FlyAbility implements Flyable{@Overridepublic void fly(){... }
}//省略Tweetable接口、Tweetability类、
//EggLayable接口和EggLayAbility类的代码实现
public class 0strich implements Tweetable, Egglayable { private TweetAbility tweetability = new Tweetabil1ty();//轮鸟类private EggLayabiliey eggLaynbility = new EggLayAbi1ity();//组合//1省略其他属性和方法@Overridepublic void tweet(){tweetAbility.tweet();//委托};@Overridepublic void layEgg(){eggLayAbility.layEgg();//委托}
}
我们知道,继承主要有3个作用:表示is-a关系、支持多态特性和代码复用。而这3个作用都可以通过其他技术手段来达成。例如,is-a关系可以通过组合和接口的has-a关系替代; 多态特性可以利用接口实现;代码复用可以通过组合和委托实现。从理论上来讲,组合、接口和委托3种技术手段完全可以替代继承。因此,在项目中,我们可以不用或少用继承关系,特别是一些复杂的继承关系。
4.如何决定是使用组合还是使用继承
尽管我们鼓励多用组合,少用继承,但组合并非完美,继承也并非一无是处。从上面的例子来看,继承改写成组合意味着要进行更细粒度的拆分。这也意味着,我们要定义更多的类和接口。类和接口的增多会增加代码的复杂程度与维护成本。因此,在实际的项目开发中,我们要根据具体的情况选择是使用继承还是使用组合。
如果类之间的继承结构稳定,不会轻易改变,而且继承层次比较浅,如最多有两层的继承关系,继承关系不复杂,我们就可以大胆地使用继承。反之,如果系统不稳定,继承层次很深,继承关系复杂,那么我们尽量使用组合替代继承。
一些特殊的场景要求必须使用继承。如果我们不能改变一个函数的入参类型,而入参又非接口,那么,为了支持多态,只能采用继承来实现。例如下面这段代码,其中的 FeignClient类是一个外部类,我们没有权限修改这部分代码,但是,我们希望能够重写这个类在运行时执行的encode()函数。这个时候,我们只能采用继承来实现。
public class FeignClient{//Feign client框架代码11...省略其他代码...public void encode(string url){...}
}
public class CustomizedFeignclient extends FeignClient {@Overridepublic void encode(string url){//...省略重写encode()的实现代码..}
}
public void demofunction(FeignClient feignClient){//...省略部分代码.feignClient.encode(url);//省略部分代码...
}//调用
FeignClient client=new CustomizedFeignClient();
demofunction(client);
之所以推荐“多用组合,少用继承”,是因为长期以来,很多程序员过度使用继承,还那句话,组合并非完美,继承也不是一无是处。控制好它们的副作用,发挥它们各自的优势在不同的场合下,恰当地选择使用继承或组合,这才是我们应该追求的。