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

复合优于继承

作者: 想飞的僵尸 | 来源:发表于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. 总结

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

    相关文章

      网友评论

        本文标题:复合优于继承

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