7、组合优于继承原则(Composite Reuse Principle)

concept

复用时要尽量使用关联关系,少用继承

在系统中应尽量多使用对象间的关联关系【了解关联关系请移步至博客 - - -详解类之间的关系 】,尽量少使用甚至不使用继承关系来达到复用已有对象的目的


analyse

聚合

聚合,它是一种弱关联,是 【整体和局部】之间的关系,且局部可以脱离整体独立存在
代表局部的对象有可能会被多个代表整体的对象所共享,而且不一定会随着某个代表整体的对象被销毁或破坏而被销毁或破坏,甚至代表局部的对象的生命周期可以超越整体

组合

组合,是一种强关联关系,整体对象和局部对象的生命周期是一样的
整体对象负责局部对象的生命周期
局部对象不能被其他对象共享;
如果整体对象被销毁或破坏,那么局部对象也一定会被销毁或破坏

换句话说,组合是值的关联(Aggregation by Value),而聚合是引用的关联(Aggregation by Reference)。


组合/聚合复用原则的来源:

在面向对象的设计中,如果直接继承基类,会破坏封装,因为继承会将基类的细节暴漏给实现类,且实现类会随着基类的改变而改变,这是不可抗逆的。所以就提出了 组合/聚合复用原则,也就是在实际的开发设计中,尽量使用 组合或者聚合 的方式代替 使用继承的方式 来复用已有的功能。即在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。

组合或聚合关系 可以将已有的对象(也可称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用,相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。


example

模拟业务场景【实现一个HashSet,可以跟踪从它被创建之后曾经添加过几个元素。】

class InstrumentedSet<E> extends HashSet<E> {
    // 设定 计数初始值
    private int count = 0;

    /**
     * 重写 父类 HashSet add方法
     */
    @Override
    public boolean add(E e) {
        count++;// 计数
        return super.add(e);
    }

    /**
     * 重写父类 HashSet addAll方法
     */
    @Override
    public boolean addAll(Collection<? extends E> c) {
        count += c.size();// 计数
        return super.addAll(c);
    }

    public int getAddCount() {
        return count;
    }
}

InstrumentedSet 类 中使用 addCount 字段记录添加元素的次数,并覆盖父类的 add()addAll() 实现。

public class Client {
    public static void main(String[] args) {
        InstrumentedSet<String> instrumentedSet = new InstrumentedSet<>();
        instrumentedSet.addAll(Arrays.asList("one", "two", "three"));
        System.out.println("添加次数:"+instrumentedSet.getAddCount());
    }
}
/** 输出为 【添加次数:6】  */

我们发现结果并不是我们所期望的那样,我们查看源码发现

public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }

addAll()是基于add()实现,在addAll()中通过调用add()方式添加元素,也就是说,我们添加"one", "two", "three"的时候,每次都计数两次。子类在扩展父类的功能时,如果不清楚实现细节,是非常危险的,况且父类的实现在未来可能是变化的,毕竟它并不是为扩展而设计的。

使用继承体系,新的实现较为容易,因为超类的大部分功能可以通过继承的关系自动进入子类。而且修改或扩展继承而来的实现较为容易。但是 继承复用破坏包装,因为继承将基类的实现都暴露给派生类; 如果基类的实现发生改变,那么派生类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,不够灵活。

继承是实现代码复用的有力手段,但它并非永远是完成这项工作的的最佳工具。

对于上述代码,优化如下

//实现Set接口中所有的方法
public class SetWrapper<E> implements Set<E> {
  //引用 Set<E> 类型的字段,将Set类关联至SetWrapper类中	
  private final Set<E> s;
  //通过 构造注入的方式,将对象s 注入至SetWrapper
  public SetWrapper(Set<E> s) { 
  	this.s = s; 
  }
  //SetWrapper对象可以直接调用s的方法,完成s的功能。
  public boolean add(E e) {
  		return s.add(e);      
  }
  public boolean addAll(Collection<? extends E> c) { 
  		return s.addAll(c);     
 }
  public void clear()               { s.clear();            }
  public boolean contains(Object o) { return s.contains(o); }
  public boolean isEmpty() { return s.isEmpty();   }
  public int size() { return s.size();      }
  public Iterator<E> iterator() { return s.iterator();  }
  public boolean remove(Object o){ return s.remove(o);   }
  public boolean containsAll(Collection<?> c) { return s.containsAll(c); }
  public boolean removeAll(Collection<?> c) { return s.removeAll(c);   }
  public boolean retainAll(Collection<?> c) { return s.retainAll(c);   }
  public Object[] toArray()          { return s.toArray();  }
  public <T> T[] toArray(T[] a)      { return s.toArray(a); }
  @Override public boolean equals(Object o) { return s.equals(o);  }
  @Override public int hashCode()    { return s.hashCode(); }
  @Override public String toString() { return s.toString(); }
}

重构后的 类SetWrapper实现Set的接口,将 Set对象以成员变量的方式引用至 对象SetWrapper,并使用 构造注入的方式将对象注入。

public class InstrumentedSet<E> extends SetWrapper<E> {
    // 设定 计数初始值
    private int count = 0;

    /**
     * 重写 父类 SetWrapper  add方法
     */
    @Override
    public boolean add(E e) {
        count++;// 计数
        return super.add(e);
    }

    /**
     * 重写父类 SetWrapper addAll方法
     */
    @Override
    public boolean addAll(Collection<? extends E> c) {
        count += c.size();// 计数
        return super.addAll(c);
    }
    public int getAddCount() {
        return count;
    }
}

InstrumentedSet继承我们自己构建的类SetWrapper,将基类改变方向 变为可控,从而降低因为父类改变而造成系统的风险


如果扩展一个类的时候,仅仅是增加了一些新的方法,而不是覆盖现有的方法,你可能认为是安全的。虽然这种扩展方式安全一些,但也并非完全没有风险,因为基类总是不可控的。如果 基类后续发行版本中,新增了一个新的方法,这个方法与子类中的某一个方法有相同的签名,但返回参数不同,那么子类编译将无法通过; 如果给子类提供的方法带有与新的超类方法完全相同的签名和返回类型,实际上就重写 了超类中的方法,这与【里氏替换】原则相违背。

幸运的是,有一种办法可以避免前面提到的所有的问题。不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计被称为“关联”,将现有的类变成了新类的一个组件,新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果。这被称为“转发”,新类中的方法被称为转发方法。这样得到的类将会非常稳固,它不依赖于现有类的实现细节

继承的功能非常强大,但是也存在许多的问题,因为它违背了封装原则。只有当子类和超类之间确实存在子类型关系时,使用继承是最适当的。即使如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性。为了避免这种脆弱性,可以用关联和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且 功能 也更加强大。


总结

组合/聚合复用优缺点和继承复用优缺点

组成/聚合复用 :

通过创建一个由其他对象组合的对象来获得新功能的重用方法,新功能的获得是通过调用组合对象的功能实现;

优点

  • 不破坏封装,整体类与局部类之间松耦合,彼此相对独立;
  • 具有较好的可扩展性;
  • 支持动态组合。在运行时, 通过获得和局部类类型相同的对象的引用,可以在运行时动态定义组合的方式;
  • 黑盒复用,局部类的实现细节是不可见的

缺点

  • 组合复用建造的系统会有较多的对象需要管理。
继承

通过扩展已实现的对象来获得新功能的重用方法,子类可以使用父类所有开放的方法,且子类本身可以提供更多的属性和方法来扩展基类

优点

  • 新的实现很容易,因为大部分是继承而来的
  • 很容易修改和扩展已有的实现

缺点

  • 打破了封装,因为基类向子类暴露了实现细节
  • 白盒重用,因为基类的内部细节通常对子类是可见的
  • 当父类的实现改变时可能要相应的对子类做出改变,且父类的改变常常是不可控的
  • 不能在运行时改变由父类继承来的实现

白盒复用

通过继承实现的代码复用常常是一种“白盒复用”, 这里的白盒指的是可见性: 对于继承来说,父类的内部实现对于子类来说是不透明的(实现一个子类时, 你需要了解父类的实现细节, 以此决定是否需要重写某个方法)

黑盒复用

对象组合实现的代码复用则是一种“黑盒复用”“: 对象的内部细节不可见,对象仅仅是以“黑盒”的方式出现(可以通过改变对象引用来改变其行为方式)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值