大家好,我是Coder哥,今日咱们来聊一下承继和组合。

昨天刷到一个问题,有人问是什么原因让rust和go等新式言语拥抱组合放弃承继?细心一想确实是,之前一向在用Java,最近才开端用Go言语,在用Java的时分有一个准则是,假如没充沛的理由不要用承继。可是在用Java开发的时分也一向很随意,习惯性的撸承继,可是Go就很不一样了,没得选,~_~!!,只能用组合。

那么现代言语为啥提倡用组合呢?其实不只是新的言语,包括Java这个老大哥,在 《Effective Java》(文末有电子书地址)的 第16条 中有这样一句话:复合优先于承继, 承继是完成代码重用的的有力手法,但它未必是最好的办法。

所以咱们是该细心的思考一下。咱们不讨论详细代码,只谈思维,我觉得能够从以下几个方向来考虑一下:

  1. 承继与组合的特色。
  2. 在实践运用中承继会带来什么问题?
  3. 组合是怎样处理这个问题的?

让咱们结合一个实践运用,经过扔掉承继,拥抱接口、拥抱组合的办法来一步步优化。信任看完这个文章你会对承继和组合有一个新的认识。

承继与组合的特色

这儿就不说承继和组合的界说了,承继和组合是面向目标编程中两种常见的代码重用办法。它们都能够完成代码的复用,可是他们有各自的优缺点。这儿先整体说一下。

承继:

优点:

  1. 它能够完成代码的重用,从父类承继的特色和办法能够在子类中直接运用。
  2. 承继链的扩展。经过承继能够构建承继链,使得子类能够承继先人类的一切特色和办法,然后进步代码的可扩展性和可维护性。
  3. 承继和组合都能够完成多态,即同一个办法在不同的子类中表现出不同的行为。

缺点:

  1. 父类的改动会影响子类。假如父类的完成发生变化,一切承继自该父类的子类都需要相应地进行修正,这会添加代码的维护成本。
  2. 承继联系的耦合度高。子类和父类之间是紧密耦合的联系,这会影响代码的灵活性和可移植性。

那么组合呢,组合相关于承继有如下特色:

组合:

优点:

  1. 组合能够削减代码的耦合性,因为目标之间的联系是松懈的,修正一个目标不会影响到其他目标。
  2. 组合能够完成更灵活的代码规划,因为能够根据需要组合不同的目标。
  3. 接口阻隔。组合能够完成接口阻隔,将不同的功用模块别离完成,进步代码的可复用性。

缺点:

  1. 代码量添加。相比于承继,组合需要添加更多的代码来完成不同的模块组合。
  2. 目标之间的交互复杂。组合联系下,目标之间的交互有时需要复杂的接口界说和完成,添加了代码的复杂度。

在实践运用中承继会带来什么问题以及咱们怎样优化它?

1、初步问题

比方说咱们要规划一个关于车的类。依照面向目标编程的思维,咱们将“车类”这样一个事务笼统成一个BaseCar类,默许有run的行为。那么一切车类都能够承继这个笼统类。比方,轿车,卡车等。

public class BaseCar {
 	//... 省掉其他特色和办法... 
 	public void run() { //... }
}
// 轿车
public class Car extends AbstractCar { 
}

可是,根据对这个目标的理解和需求,车出了跑,还能够修轮胎,能够修引擎等。那么AbstractCar就变成如下的类,那么这个时分有个自行车的类需要完成,自行车不能修引擎,要怎样写呢

public class BaseCar {
 	//... 省掉其他特色和办法... 
 	public void run() { //跑... }
  public void repaireTire() { //修轮胎... }
  public void repaireEngine() { //修引擎... }
}
// 自行车
public class Bicycle extends BaseCar { 
 //... 省掉其他特色和办法... 
 public void repaireEngine() { 
     throw new UnSupportedMethodException("我没有引擎!");  
 }
}

依照这个逻辑,上面的代码看似处理了问题,实则可能会堆成屎山,上面的规划有三个点有很大隐患:

第一个是,假如咱们把基类的行为完成都放到基类里边,比方说,假如后边添加了自动驾驶功用全景功用,带天窗功用,那是不是都要堆到基类里边了,虽然能进步复用性,可是也会改动一切子类的功用,这也会导致代码的复杂性提升,这点是咱们并不想看到的。

第二个点是,关于没有没有那些功用的目标,比方自行车,就不应该把修引擎的功用暴露到自行车类里边。

第三个点是,假如扩展到其他目标怎样办,比方说人也会跑,飞机也会跑。那么这个规划后边就欠好扩展了,也不够灵活。

那么关于上面的问题咱们要怎样处理呢。你是不是想到了接口,对,接口更多的是行为的界说,笼统类更多的是界说的某一类类型的根底通用行为的完成。其实笼统类的加入也进步了代码的复杂度。

2、接口优化

关于以上问题,咱们无视详细目标,只看行为,比方,跑,修引擎,修轮胎,这些功用行为,咱们能够界说成接口:IRun,IEngine,ITire,这三个接口:

public interface IRun {
  void run();
}
public interface IEngine {
  void repaireEngine();
}
public interface ITire {
  void repaireTire();
}

那么咱们完成轿车这个类的时分能够,完成IRun、IEngine、ITire 这三个接口,咱们完成自行车类的时分能够完成IRun这一个接口,那么假如想写个人类的目标的时分,也只需要完成IRun 这个接口就能够了。

public class Car implements IRun, IEngine, ITire {//轿车
  //... 省掉其他特色和办法...
  @Override
  public void run() { //跑... }
  @Override
  public void repaireEngine() { //修引擎... }
  @Override
  public void repaireTire() { //修轮胎... }
}
public class Bicycle impelents IRun, ITire{//自行车
  //... 省掉其他特色和办法...
  @Override
  public void run() { //跑... }
  @Override
  public void repaireTire() { //修轮胎... }
}
public class Person impelents IRun {//人
  //... 省掉其他特色和办法...
  @Override
  public void run() { //跑... }
}

这样是不是灵活性就更好了,到这是不是理解了为啥Go、Rust等现代言语,踢出了承继,踢出了笼统类,保留了接口完成的原因了吧。

可是,只看上面代码好像还有问题,那每个目标都要写一遍run,repaireEngine,repaireTire等功用,这样岂不是很麻烦,说好的复用呢???别急,组合该登场了。

3、组合优化

关于上面的问题,咱们能够经过先完成接口,然后经过组合、托付的办法来处理。代码如下:

public class CarRunEnable implements IRun {
  @Override
  public void run() { // 车跑... }
}
public class PersonRunEnable implements IRun {
  @Override
  public void run() { // 人跑... }
}
//省掉其他完成 EngineEnable/TireEnable
public class Car implements IRun, IEngine, ITire {//轿车
  private CarRunEnable runEnable = new CarRunEnable(); //组合
  private EngineEnable engineEnable = new EngineEnable(); //组合
  private TireEnable tireEnable = new TireEnable(); //组合
  //... 省掉其他特色和办法...
  @Override
  public void run() { //跑... 
    runEnable.run();
  }
  @Override
  public void repaireEngine() { //修引擎...
    engineEnable.repaireEngine();
  }
  @Override
  public void repaireTire() { //修轮胎... 
    tireEnable.repaireTire();
  }
}
public class Bicycle impelents IRun {//自行车
  private CarRunEnable runEnable = new CarRunEnable(); //组合
  private TireEnable tireEnable = new TireEnable(); //组合
  //... 省掉其他特色和办法...
  @Override
  public void run() { //跑... 
    runEnable.run();
  }
  @Override
  public void repaireTire() { //修轮胎... 
    tireEnable.repaireTire();
  }
}
public class Person impelents IRun {//人
  private PersonRunEnable runEnable = new PersonRunEnable(); //组合
  //... 省掉其他特色和办法...
  @Override
  public void run() { //跑... 
    runEnable.run();
  }
}

看上面的代码逻辑是不是就很直爽,加功用,随意加,不影响其他的类,耦合度降低了很多,可是内聚性也不用承继差,这就是所谓的高内聚低耦合。唯一的缺点是,代码质变多了。

那么咱们回到最开端的那个问题,什么原因让rust和go等新式言语拥抱组合放弃承继?,信任你心中已经有了答案。

到最后了,感谢各位能看到这儿。

参阅书本:
《Effective Java》: www.todocoder.com/pdf/java/00…
《Java编程思维》:www.todocoder.com/pdf/java/00…