来源:blog.csdn.net/fuzhongmin05/article/details/108646872在面向对象编程中,有一个非常经典的设计原则,那就是:组合优于继承,多用组合,少用继承。同理,《阿里巴巴Java开发手册》中也有一条规则:扩展慎用继承,先用组合。这个可以在公众号Java核心技术:手册得到解答,获取最新高清完整版PDF。为什么不推荐继承?大家在初学面向对象编程的时候都会有这样的感受:继承可以实现类的重用。所以很多开发者在需要复用一些代码的时候自然会使用类的继承方式,因为书上是这么写的。继承是面向对象的四大特性之一,用来表示类之间的is-a关系,可以解决代码重用问题。继承虽然有很多作用,但是如果继承层次太深太复杂,也会影响代码的可维护性。假设我们要设计一个关于鸟类的课程。我们将“鸟”这个抽象概念定义为一个抽象类AbstractBird。所有细分的鸟类,如麻雀、鸽子、乌鸦等,都继承了这个抽象类。我们知道大多数鸟都会飞,那我们可不可以在AbstractBird抽象类中定义一个fly()方法呢?答案是否定的。虽然大多数鸟类都能飞翔,但也有例外,例如鸵鸟,它们不会飞。鸵鸟用fly()方法继承父类,所以鸵鸟有“飞”的行为,这显然是错误的。如果在ostrich的子类中重写fly()方法,让它抛出UnSupportedMethodException怎么办?具体代码实现如下:publicclassAbstractBird{//...省略其他属性和方法...publicvoidfly(){//...}}publicclassOstrichextendsAbstractBird{//Ostrich//。..省略其他属性和方法...publicvoidfly(){thrownewUnSupportedMethodException("Ican'tfly.'");}}这种写法虽然可以解决问题,但是不够优雅。因为除了鸵鸟,还有很多不会飞的鸟类,比如企鹅。对于这些不会飞的鸟,全部重写fly()方法并抛出异常,完全是代码重复。理论上,这些不会飞的鸟根本不应该有fly()方法,将fly()接口暴露在不会飞的鸟外面会增加被误用的概率。为解决上述问题,必须将AbstractBird类派生为两个更细分的抽象类:会飞的鸟AbstractFlyableBird和不会飞的鸟AbstractUnFlyableBird,这样麻雀、乌鸦等会飞的鸟继承AbstractFlyableBird,让不会飞的鸟如鸵鸟和企鹅继承了AbstractUnFlyableBird类。具体的继承关系如下图所示:这样一来,继承关系就变成了三层。但如果我们不仅仅关注“鸟会不会飞”,还要继续关注“鸟会不会唱歌”,如果我们把鸟细分得更细呢?两种注意行为的自由组合会产生四种情况:飞又叫、不飞又叫、飞不叫、不飞不叫。如果继续沿用刚才的设计思路,继承层次又会加深。如果继续增加“鸟会不会下蛋”的行为,类的继承层次会越来越深,继承关系也会越来越复杂。而这种深层次复杂的继承关系,一方面会导致代码的可读性很差。因为我们要找出某个类有哪些方法和属性,所以必须要读父类的代码,父类的父类的代码……一路回到最上面的父类的代码.另一方面,这也打破了类的封装特性,将父类的实现细节暴露给子类。子类的实现依赖于父类的实现,两者是高度耦合的。一旦修改了父类的代码,就会影响到所有子类的逻辑。继承最大的问题是,当继承层次太深,继承关系太复杂时,会影响代码的可读性和可维护性。组合比继承有什么优势?可重用性是面向对象技术的巨大潜在好处之一。如果用得好,可以帮助我们节省大量的开发时间,提高开发效率。但是,如果被滥用,它会产生大量不可维护的代码。作为一种面向对象的开发语言,代码重用是Java吸引人的特性之一。Java代码的重用具有三种具体的实现形式:继承、组合和委托。对于上面提到的继承带来的问题,可以结合使用组合、接口和委托这三种技术手段来解决。一个接口代表一种行为。对于“飞”这个行为特征,我们可以定义一个Flyable接口,只让会飞的鸟实现这个接口。对于会叫会下蛋等行为特征,我们可以类似地定义Tweetable接口和EggLayable接口。如果我们把这个设计思想翻译成Java代码,它会是这样的:Tweetable,EggLayable{//Ostrich//...省略其他属性和方法...@Overridepublicvoidtweet(){//...}@OverridepublicvoidlayEgg(){//...}}publicSparrow类实现Flayable、Tweetable、EggLayable{//Sparrow//...省略其他属性和方法...@Overridepublicvoidfly(){//...}@Overridepublicvoidtweet(){//...}@OverridepublicvoidlayEgg(){//...}}但是,接口只声明方法,不声明实现。也就是说,每只下蛋的鸟都必须再次实现layEgg()方法,而且实现逻辑几乎是一样的(可能在极少数场景下会有所不同),这就会导致代码重复的问题。那么如何解决这个问题呢?有以下两种方法。使用委托为三个接口定义了三个实现类,它们分别是:实现fly()方法的FlyAbility类、实现tweet()方法的TweetAbility类、实现layEgg()方法的EggLayAbility类。然后,通过组合和委托技术消除代码重复。publicinterfaceFlyable{voidfly();}publicclassFlyAbilityimplementsFlyable{@Overridepublicvoidfly(){//...}}//省略Tweetable/TweetAbility/EggLayable/EggLayAbilitypublicclassOstrichimplementsTweetable,EggLayable{//鸵鸟privateTweetAbilitytweetAbility=newTweetAbility();//组合privateEggLayAbilityeggLayAbility=newEggLayAbility();//组合//...省略其他属性和方法...@Overridepublicvoidtweet(){tweetAbility.tweet();//委托}@OverridepublicvoidlayEgg(){eggLayAbility.layEgg();//Delegate}}使用Java8的接口默认方法在Java8中,我们可以在接口中编写默认的实现方法。使用关键字default来定义默认接口实现。当然,这个默认方法也可以被覆盖。publicinterfaceFlyable{defaultvoidfly(){//默认实现...}}publicinterfaceFlyable{defaultvoidfly(){//默认实现...}}publicinterfaceTweetable{defaultvoidtweet(){//默认实现...}}publicinterfaceEggLayable{defaultvoidlayEgg(){//默认实现...}}publicclassOstrichimplementsTweetable,EggLayable{//Ostrich//...省略其他属性和方法...}publicclassSparrowimplementsFlayable,Tweetable,EggLayable{//Sparrow//...省略其他属性和方法...}继承主要有三个功能:表达is-a关系,支持多态特性,代码重用。而这三个功能可以通过其他技术手段来实现。例如,is-a关系可以替换为组合和接口的has-a关系;多态也可以通过接口来实现;代码重用可以通过组合和委托来实现。因此,理论上,通过组合、接口、委托这三种技术手段,我们可以完全替代继承,在项目中不使用或少使用继承关系,尤其是一些复杂的继承关系。如何判断是用组合还是继承虽然我们提倡多用组合,少用继承,但是组合并不完美,继承也不是没有用。从上面的例子来看,将继承重写为组合意味着拆分更细粒度的类。这也意味着我们必须定义更多的类和接口。类和接口的增加或多或少都会增加代码的复杂度和维护成本。如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如最多有两层继承关系),继承关系不复杂,就可以大胆使用继承。反之,系统越不稳定,继承层次越深,继承关系越复杂,我们就尽量用组合代替继承。此外,还有一些设计模式以固定的方式使用继承或组合。例如,装饰者模式(decoratorpattern)、策略模式(strategypattern)、复合模式(compositepattern)等都使用了组合关系,而模板模式(templatepattern)则使用了继承关系。有些地方提到组合优先继承的软件开发原则时,可能会说“多用组合,少用继承”。所谓多用和少用,其实是指在特定的场景下,搞清楚到底需要哪个。对于软件开发原则等问题,不宜拘泥于言辞。其实在《Thinking in Java》中有提到,当你使用继承的时候,肯定要使用多态特性。比如你要写一个绘图系统来绘制不同的图形,这时候你可能会考虑调用相应的函数时不需要考虑具体的类型,直接绘制即可。具体图形留给运行时判断。这时候就用到了多态性,需要有继承关系。一个父类,多个子类。然后用父类的类型来引用具体子类的对象。而当不使用多态时,继承有什么用呢?代码复用?一个继承可以让你少写很多代码,但是如果用错了地方,后期的维护可能会是灾难性的。因为继承关系的耦合度很高,一个改动就会导致处处需要修改。这时候就需要组合了。所以我坚持认为如果你不想使用多态特性,继承是没有用的。尴尬处境中的继承大家对继承的反感主要是因为长期以来程序员过度使用继承,继承并不是没有用的。在一些特殊情况下,我们必须使用继承。如果不能改变函数的入参类型,并且入参不是接口,为了支持多态,只能采用继承。比如下面这段代码,其中FeignClient是一个外部类,我们不能修改这个外部类,而是希望在运行时重写这个类执行的encode()函数。这时,我们只能使用继承来实现。publicclassFeignClient{//FeignClient框架代码,只读,不可修改//...省略其他代码...publicvoidencode(Stringurl){//...}}publicvoiddemofunction(FeignClientfeignClient){//...feignClient.encode(url);//...}publicclassCustomizedFeignClientextendsFeignClient{@Overridepublicvoidencode(Stringurl){//...重写encode...的实现}}//调用FeignClientclient=newCustomizedFeignClient();演示功能(客户端);上面的例子不是很恰当,更像是不得已而为之。这恰恰反映了面向对象编程大多数场景下继承的尴尬处境。其实我们很难真正用好继承。其根本原因在于,在自然界中,世代之间、物种之间都存在着变异,而且这种变化无法用规律的方式来描述,而且伴随着某些功能的增强,也伴随着一些功能的弱化,甚至是一些功能的改变。早期的软件行业,软件功能较差,需要不断添加软件功能来满足需求。这时,继承关系可以体现出软件迭代后增强功能的特点。但很快就到了瓶颈期。功能不再是衡量软件质量的主要指标。各种差异化的体验变得更加重要。这时候,软件迭代不再是功能的简单堆积,甚至完全推翻。再次,编程语言中的继承关系将被丢弃。注:以上关于组合和继承的代码示例来自极客时间王征老师近期热点文章推荐的《设计模式之美》第10讲:1.1,000+Java面试题及答案(2022最新版)2.惊艳!Java协程来了。..3.SpringBoot2.x教程,太全面了!4.不要用爆破爆满画面,试试装饰者模式,这才是优雅的方式!!5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!
