复合优于继承

第16条:复合优于继承


前言

本条内容的继承不包括接口继承。

1.什么事复合

复合就是在你的类中添加一个私有域,引用一个类的实例,使被引用类成为引用类的一个组件。

2. 继承的缺点

(1)继承不容易控制,使用不当容易导致软件非常脆弱,特别是继承不再同一个包下的类。
(2)继承打破了父类的封装性,有的时候父类的内部实现改变,可能会导致子类遭到破坏。
举个比书上简单一点的例子,比如我们有个类,他包含一个集合,我们要并对外提供了两个api,分别是add(String str)和addAll(List<String> strs),具体的类如下:

public class MyObject {
  private List<String> list = new ArrayList<>();

  public void add(String ele) {
    list.add(ele);
  }
  
  public void addAll(List<String> elements) {
    for(String ele : elements) {
      add(ele);
    }
  }
}

然后我们需要记录这个类的从创建到销毁,一共添加过多少元素,如果我们想要用继承的方式,并且在不知道具体内部实现的前提之下,我们可能会这样写:

public class MyChildObject extends MyObject  {
  private int addedEleNum = 0;
  
  @Overried
  public void add(String ele) {
    addedEleNum++;
    super.add(ele);
  }
  
  @Overried
  public void addAll(List<String> elements) {
    addedEleNum += elements.size();
    super.addAll(elements);
  }
}

很明显,这样做是得不到我们想要的结果的,想要得到我们想要的结果,我们一般需要查看MyObject的具体实现,这就打破了封装性,好吧,看了具体实现之后我们知道怎么做了,那就是不覆盖addAll()方法。那问题又来了,如果在下一个版本中,MyObject的addAll()方法改了呢,改成想下面这样的:

public void addAll(List<String> elements) {
  for(String ele : elements) {
    list.add(ele);
  }
}

这样的话MyChildObject又不能正常工作了,OMG。导致子类不能正常工作的原因还有很多,甚至父类中新添加一个类似add()的方法都会导致子类不能正常工作。所以这样的子类是异常脆弱的。
so,可以被继承的类要么在同一个包内(在同一个程序员的控制之下),要么是专门为继承而实际,并提供了很好的文档说明。

(3)看了第二点,你可能会觉得,我继承的时候只要不覆盖父类的方法不就可以了么?确实,相对于覆盖确实安全一些,不过这不是绝对安全,当父类新增了一个方法,并且方法名和和参数都和父类相同,但返回值不同,那么子类将无法通过编译。如果返回值也相同的话,又回到了第二个问题。同样导致了子类不健壮。

3.复合的优点

上面说到继承的缺点就是复合的优点。

4.复合的正确使用姿势

在这里需要先解释一下“转发“的概念,转发就是,你先复合一个类,然后在复合的类中实现所有被复合类的公有方法(api),实现的方式就是在相应的方法中调用被复合类的方法,并且不能被添加其他方法。比如为上面的MyObject写一个转发类:

public class ForwardingMyObject {
  private MyObject mObject;
  
  //这里使用依赖注入的方式来得到被复合类的引用
  //目的是提高可测试性和灵活性
  public ForwardingMyObject(Myobject object) {
    this.mObject = object;
  }
  
  public void add(String ele) {
    mObject.add(ele);
  }

  public void addAll(List<String> elements) {
    mObject.addAll(elements);
  }
}

转发类就是上面提到的,专门为继承而设计的类
现在来阐述复合的正确使用姿势:
(1)为想要被继承的类设计一个转发类。
(2)继承这个转发类。
(3)覆盖想要覆盖的方法,或者添加想要添加的方法。
将例子写完,我们来快乐的继承ForwardingMyObject吧:

public class MyChildObject extends ForwardingMyObject {
  private int count = 0;  

  public MyChildObject(MyObject object) {
    super(object);
  }

  @Override
  public void add(String ele) {
    count ++;
    super.add(ele);
  }

  @Override
  public void addAll(List<String> elements) {
    cout += elements.size();
    super.addAll(elements);
  }
}

为什么不直接在在转发类中直接实现计数功能?这样好麻烦!
好吧,我承认,上面的例子太简单,不利于解释这个问题,主要是为了便于理解,那我们继续。
首先问个问题,我们在设计一个类的api的时候是直接在类中写一堆public的方法么?
什么?是的,好吧,你这种没追求的程序员快滚去睡觉吧,我不想和你聊天T_T。
我们在设计一个类的api的时候一般都是先将类的接口写出来,然后在用这个类来实现这个接口。
行,明白这里我们就来看书里的栗子吧,这里我把set改成list,联系下上文中的栗子:
转发类

public class ForwardingList<E> implements List<E> {
  private List<E> mList;
  public ForwardingList(List<E> list) {
    this.mList = list;
  }
  @Override
  public void add(int location, E object) {
    mList.add(location, object);
  }
  @Override
  public boolean add(E object) {
    return mList.add(object);
  }
  //其他的一些api
  ...
}

包装类(相当于上面例子中的MyChildObject):

public class InstrumentedList<E> extends ForwardingList<E> {

  private int addCount = 0;

  public InstrumentedList(List<E> list) {
    super(list);
  }

  @Override
  public boolean add(E object) {
    addCount++;
    return super.add(object);
  }

  @Override
  public void add(int location, E object) {
    addCount++;
    super.add(location, object);
  }

  @Override
  public boolean addAll(@NonNull Collection<? extends E> collection) {
    addCount += collection.size();
    return super.addAll(collection);
  }

  @Override
  public boolean addAll(int location, @NonNull Collection<? extends E> collection) {
    addCount += collection.size();
    return super.addAll(location, collection);
  }

  public int getAddCount() {
    return addCount;
  }
}

看到好处了么?现在我们写的InstrumentedList是一个真正List,不仅仅只是名字里有List而已!这意味着任何需要List作为参数的地方都可以把他传递过去!
不仅如此,它实现了一个传入List的构造方法,也就是说只要是实现了List的类都可以传递进去,什么ArrayList呀LinkedList都可以传进去并统计add了多少个元素。十分灵活!

5.复合的缺点

不适合用于回调框架。

6. 总结

简而言之,继承的功能十分强大,但也存在诸多问题,因为他违背了封装原则。只有当子类和超类确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类存在不同的包中,并且超类并不是为继承而设计的,那么继承将导致脆弱性,可以用复合和转转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 文中的实验代码我放在了这个项目中。 以下内容是我通过整理[这篇博客] (http://yulingtianxia....
    茗涙阅读 948评论 0 6
  • (一)Java部分 1、列举出JAVA中6个比较常用的包【天威诚信面试题】 【参考答案】 java.lang;ja...
    独云阅读 7,143评论 0 62
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,778评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,991评论 19 139
  • 在写移动端web项目时,经常会用到REM这个单位。但是不同REM和PX的相对基准值是不同的(像我们在IPhone6...
    欢乐相随阅读 531评论 0 0