美文网首页程序员
复合优于继承

复合优于继承

作者: 想飞的僵尸 | 来源:发表于2016-07-24 14:20 被阅读361次

第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. 总结

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

相关文章

  • 复合优于继承

    第16条:复合优于继承 前言 本条内容的继承不包括接口继承。 1.什么事复合 复合就是在你的类中添加一个私有域,引...

  • Effective Java学习笔记-继承篇

    Section 4 Article 14 复合优于继承 不适当的使用(超类并不是为了扩展而设计)继承会导致脆弱的软...

  • 组合优于继承

    《Effective Java 中文版第2版》书中第16条中说到: 继承是实现代码复用的有力手段,但它并非永远是完...

  • 组合和继承

    需要一个类的某个功能使用组合需要那个类的所有功能使用继承组合优于继承,接口优于实现

  • Boolan C++笔记 三

    -Inheritance(继承) -Composition(复合) -Delegation(委托) 复合关系下的构...

  • Boolan_c++第3周笔记

    一、继承,复合,委托(类与类之间的关系) 1. Composition(复合): A拥有B即为复合 templat...

  • GeekBand C++第三周学习感悟

    本周主要讲了继承 委托和复合 复合就是一个类中成员变量中拥有其他类的对象。 继承就是A继承B A拥有了B的特性, ...

  • OOP[GeekBand]

    1 面向对象的三把大刀 -复合、委托和继承 1.1复合(Composition) 1.1.1 定义 复合表示的是h...

  • C++中类的组合(BOOLAN教育)

    Inheritance(继承),Composition(复合),Delegation(委托) Compositio...

  • 面向对象(七)组合优于继承?

    组合优于继承,多用组合少用继承。 1、为什么不推荐使用继承? 继承是面向对象的四大特性之一,用来表示类之间的 is...

网友评论

    本文标题:复合优于继承

    本文链接:https://www.haomeiwen.com/subject/oabujttx.html