在面向对象编程中,有一条很是经典的设计原则,那就是:组合优于继承,多用组合少用继承。为何不推荐使用继承?组合相比继承有哪些优点?如何判断该用组合仍是继承?java
继承是面向对象的四大特性之一,用来表示类之间的 is-a
关系,能够解决代码复用的问题。虽然继承有诸多做用,但继承层次过深、过复杂,也会影响到代码的可维护性。因此,对因而否应该在项目中使用继承,网上有不少争议。不少人以为继承是一种反模式,应该尽可能少用,甚至不用。为何会有这样的争议?数据库
假设咱们要设计一个关于鸟的类。咱们将“鸟类”这样一个抽象的事物概念,定义为一个抽象类 AbstractBird
。全部更细分的鸟,好比麻雀、鸽子、乌鸦等,都继承这个抽象类。编程
咱们知道,大部分鸟都会飞,那咱们可不能够在 AbstractBird
抽象类中,定义一个 fly()
方法呢?答案是否认的。尽管大部分鸟都会飞,但也有特例,好比鸵鸟就不会飞。鸵鸟继承具备 fly()
方法的父类,那鸵鸟就具备“飞”这样的行为,这显然不符合咱们对现实世界中事物的认识。固然,你可能会说,在鸵鸟这个子类中重写(override
)fly()
方法,让它抛出 UnSupportedMethodException
异常不就能够了吗?具体的代码实现以下所示:设计模式
public class AbstractBird { //...省略其余属性和方法... public void fly() { //... } } public class Ostrich extends AbstractBird { //鸵鸟 //...省略其余属性和方法... public void fly() { throw new UnSupportedMethodException("I can't fly.'"); } }
这种设计思路虽然能够解决问题,但不够优美。由于除了鸵鸟以外,不会飞的鸟还有不少,好比企鹅。对于这些不会飞的鸟来讲,都须要重写 fly()
方法,抛出异常。这样的设计,一方面,徒增了编码的工做量;另外一方面,也违背了最小知识原则(Least Knowledge Principle
,也叫最少知识原则或者迪米特法则),暴露不应暴露的接口给外部,增长了类使用过程当中被误用的几率。架构
那再经过 AbstractBird
类派生出两个更加细分的抽象类:会飞的鸟类 AbstractFlyableBird
和不会飞的鸟类 AbstractUnFlyableBird
,让麻雀、乌鸦这些会飞的鸟都继承 AbstractFlyableBird
,让鸵鸟、企鹅这些不会飞的鸟,都继承 AbstractUnFlyableBird
类,不就能够了吗?具体的继承关系以下图所示:
从图中能够看出,继承关系变成了三层。不过,总体上来说,目前的继承关系还比较简单,层次比较浅,也算是一种能够接受的设计思路。再继续加点难度。在刚刚这个场景中,咱们只关注“鸟会不会飞”,但若是咱们还关注“鸟会不会叫”,那这个时候,又该如何设计类之间的继承关系呢?框架
是否会飞?是否会叫?两个行为搭配起来会产生四种状况:会飞会叫、不会飞会叫、会飞不会叫、不会飞不会叫。若是咱们继续沿用刚才的设计思路,那就须要再定义四个抽象类(AbstractFlyableTweetableBird
、AbstractFlyableUnTweetableBird
、AbstractUnFlyableTweetableBird
、AbstractUnFlyableUnTweetableBird
)。
若是还须要考虑“是否会下蛋”这样一个行为,那估计就要组合爆炸了。类的继承层次会愈来愈深、继承关系会愈来愈复杂。而这种层次很深、很复杂的继承关系,一方面,会致使代码的可读性变差。由于咱们要搞清楚某个类具备哪些方法、属性,必须阅读父类的代码、父类的父类的代码……一直追溯到最顶层父类的代码。另外一方面,这也破坏了类的封装特性,将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,二者高度耦合,一旦父类代码修改,就会影响全部子类的逻辑。ide
总之,继承最大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。这也是为何不推荐使用继承。那刚刚例子中继承存在的问题,又该如何来解决呢?函数
实际上,能够利用组合(composition
)、接口、委托(delegation
)三个技术手段,一起来解决刚刚继承存在的问题。this
前面讲到接口的时候说过,接口表示具备某种行为特性。针对“会飞”这样一个行为特性,咱们能够定义一个 Flyable
接口,只让会飞的鸟去实现这个接口。对于会叫、会下蛋这些行为特性,咱们能够相似地定义 Tweetable
接口、EggLayable
接口。将这个设计思路翻译成 Java
代码的话,就是下面这个样子:编码
public interface Flyable { void fly(); } public interface Tweetable { void tweet(); } public interface EggLayable { void layEgg(); } public class Ostrich implements Tweetable, EggLayable {//鸵鸟 //... 省略其余属性和方法... @Override public void tweet() { //... } @Override public void layEgg() { //... } } public class Sparrow impelents Flayable, Tweetable, EggLayable {//麻雀 //... 省略其余属性和方法... @Override public void fly() { //... } @Override public void tweet() { //... } @Override public void layEgg() { //... } }
不过,接口只声明方法,不定义实现。也就是说,每一个会下蛋的鸟都要实现一遍 layEgg()
方法,而且实现逻辑是同样的,这就会致使代码重复的问题。那这个问题又该如何解决呢?
能够针对三个接口再定义三个实现类,它们分别是:实现了 fly()
方法的 FlyAbility
类、实现了 tweet()
方法的 TweetAbility
类、实现了 layEgg()
方法的 EggLayAbility
类。而后,经过组合和委托技术来消除代码重复。具体的代码实现以下所示:
public interface Flyable { void fly(); } public class FlyAbility implements Flyable { @Override public void fly() { //... } } //省略Tweetable/TweetAbility/EggLayable/EggLayAbility public class Ostrich implements Tweetable, EggLayable {//鸵鸟 private TweetAbility tweetAbility = new TweetAbility(); //组合 private EggLayAbility eggLayAbility = new EggLayAbility(); //组合 //... 省略其余属性和方法... @Override public void tweet() { tweetAbility.tweet(); // 委托 } @Override public void layEgg() { eggLayAbility.layEgg(); // 委托 } }
继承主要有三个做用:表示 is-a
关系,支持多态特性,代码复用。而这三个做用均可以经过其余技术手段来达成。好比 is-a
关系,咱们能够经过组合和接口的 has-a
关系来替代;多态特性咱们能够利用接口来实现;代码复用咱们能够经过组合和委托来实现。因此,从理论上讲,经过组合、接口、委托三个技术手段,咱们彻底能够替换掉继承,在项目中不用或者少用继承关系,特别是一些复杂的继承关系。
尽管咱们鼓励多用组合少用继承,但组合也并非完美的,继承也并不是一无可取。从上面的例子来看,继承改写成组合意味着要作更细粒度的类的拆分。这也就意味着,咱们要定义更多的类和接口。类和接口的增多也就或多或少地增长代码的复杂程度和维护成本。因此,在实际的项目开发中,咱们仍是要根据具体的状况,来具体选择该用继承仍是组合。
若是类之间的继承结构稳定(不会轻易改变),继承层次比较浅(好比,最多有两层继承关系),继承关系不复杂,咱们就能够大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,咱们就尽可能使用组合来替代继承。
除此以外,还有一些设计模式会固定使用继承或者组合。好比,装饰者模式(decorator pattern
)、策略模式(strategy pattern
)、组合模式(composite pattern
)等都使用了组合关系,而模板模式(template pattern
)使用了继承关系。
前面讲到继承能够实现代码复用。利用继承特性,咱们把相同的属性和方法,抽取出来,定义到父类中。子类复用父类中的属性和方法,达到代码复用的目的。可是,有的时候,从业务含义上,A
类和 B
类并不必定具备继承关系。好比,Crawler
类和 PageAnalyzer
类,它们都用到了 URL
拼接和分割的功能,但并不具备继承关系(既不是父子关系,也不是兄弟关系)。仅仅为了代码复用,生硬地抽象出一个父类出来,会影响到代码的可读性。若是不熟悉背后设计思路的同事,发现 Crawler
类和 PageAnalyzer
类继承同一个父类,而父类中定义的却只是 URL
相关的操做,会以为这个代码写得莫名其妙,理解不了。这个时候,使用组合就更加合理、更加灵活。具体的代码实现以下所示:
public class Url { //...省略属性和方法 } public class Crawler { private Url url; // 组合 public Crawler() { this.url = new Url(); } //... } public class PageAnalyzer { private Url url; // 组合 public PageAnalyzer() { this.url = new Url(); } //.. }
还有一些特殊的场景要求咱们必须使用继承。若是你不能改变一个函数的入参类型,而入参又非接口,为了支持多态,只能采用继承来实现。好比下面这样一段代码,其中 FeignClient
是一个外部类,咱们没有权限去修改这部分代码,可是咱们但愿能重写这个类在运行时执行的 encode()
函数。这个时候,咱们只能采用继承来实现了。
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);
尽管有些人说,要杜绝继承,100%
用组合代替继承,可是这里的观点没那么极端!之因此“多用组合少用继承”这个口号喊得这么响,只是由于,长期以来,过分使用继承。仍是那句话,组合并不完美,继承也不是一无可取。只要咱们控制好它们的反作用、发挥它们各自的优点,在不一样的场合下,恰当地选择使用继承仍是组合,这才是咱们所追求的境界。
1. 为何不推荐使用继承?
继承是面向对象的四大特性之一,用来表示类之间的 is-a
关系,能够解决代码复用的问题。虽然继承有诸多做用,但继承层次过深、过复杂,也会影响到代码的可维护性。在这种状况下,咱们应该尽可能少用,甚至不用继承。
2. 组合相比继承有哪些优点?
继承主要有三个做用:表示 is-a
关系,支持多态特性,代码复用。而这三个做用均可以经过组合、接口、委托三个技术手段来达成。除此以外,利用组合还能解决层次过深、过复杂的继承关系影响代码可维护性的问题。
3. 如何判断该用组合仍是继承?
尽管咱们鼓励多用组合少用继承,但组合也并非完美的,继承也并不是一无可取。在实际的项目开发中,咱们仍是要根据具体的状况,来选择该用继承仍是组合。若是类之间的继承结构稳定,层次比较浅,关系不复杂,咱们就能够大胆地使用继承。反之,咱们就尽可能使用组合来替代继承。除此以外,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。
MVC
架构开发 Web
应用的时候,常常会在数据库层定义 Entity
,在 Service
业务层定义 BO
(Business Object
),在 Controller
接口层定义 VO
(View Object
)。大部分状况下,Entity
、BO
、VO
三者之间的代码有很大重复,但又不彻底相同。该如何处理 Entity
、BO
、VO
代码重复的问题呢?参考:为什么说要多用组合少用继承?如何决定该用组合仍是继承?
本文由博客一文多发平台 OpenWrite 发布!