复合优于继承
继承打破了封装性(子类依赖父类中特定功能的实现细节)
合理的使用继承的情况:
- 在包内使用
- 父类专门为继承为设计,并且有很好的文档说明,存在is-a关系
只有当子类真正是父类的子类型时,才适合用继承。
对于两个类A和B,只有两者之间存在"is-a"关系,类B才能拓展类A。
继承机制会把父类API中的所有缺陷传播到子类中,而复合允许设计新的API来隐藏这些缺陷。
复合(composition):不扩展现有的类,而是在新的类中增加一个私有域,引用现有类的一个实例。
转发(fowarding):新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回结果。
1 public class FowardSetimplements Set { #转发类,被装饰类 2 3 //引用现有类的实例,增加私有域 4 private final Set set; 5 6 public FowardSet(Set set){ 7 this.set = set; 8 } 9 10 11 /* 12 *转发方法 13 */ 14 @Override 15 public int size() { 16 return set.size(); 17 } 18 19 @Override 20 public boolean isEmpty() { 21 return set.isEmpty(); 22 } 23 24 @Override 25 public boolean contains(Object o) { 26 return set.contains(o); 27 } 28 29 @NotNull 30 @Override 31 public Iterator iterator() { 32 return set.iterator(); 33 } 34 35 @NotNull 36 @Override 37 public Object[] toArray() { 38 return set.toArray(); 39 } 40 41 @NotNull 42 @Override 43 public T[] toArray(T[] a) { 44 return set.toArray(a); 45 } 46 47 @Override 48 public boolean add(E e) { 49 return set.add(e); 50 } 51 52 @Override 53 public boolean remove(Object o) { 54 return set.remove(o); 55 } 56 57 @Override 58 public boolean containsAll(Collection> c) { 59 return set.containsAll(c); 60 } 61 62 @Override 63 public boolean addAll(Collection extends E> c) { 64 return set.addAll(c); 65 } 66 67 @Override 68 public boolean retainAll(Collection> c) { 69 return set.retainAll(c); 70 } 71 72 @Override 73 public boolean removeAll(Collection> c) { 74 return set.removeAll(c); 75 } 76 77 @Override 78 public void clear() { 79 set.clear(); 80 } 81 82 @Override 83 public boolean equals(Object obj) { 84 return set.equals(obj); 85 } 86 87 @Override 88 public String toString() { 89 return set.toString(); 90 } 91 92 @Override 93 public int hashCode() { 94 return set.hashCode(); 95 } 96 } 97 98 /* 99 * 包装类(wrapper class),采用装饰者模式 100 */ 101 public class InstrumentedSet extends FowardSet { 102 private int addCount=0; 103 104 public InstrumentedSet(Set set) { 105 super(set); 106 } 107 108 @Override 109 public boolean add(E e) { 110 addCount++; 111 return super.add(e); 112 } 113 114 @Override 115 public boolean addAll(Collection extends E> c) { 116 addCount+=c.size(); 117 return super.addAll(c); 118 } 119 120 public int getAddCount() { 121 return addCount; 122 } 123 }
上面的例子中,FowardingSet是转发类,也是被包装类,而InstrumentedSet是包装类,它采用的是装饰者模式,而不是委托模式。
装饰者模式:
装饰者模式挺像一种组合、而且是可以任意搭配、制定的。当我们有新的需求的时候、添加一个装饰器就ok。必要的时候可以添加组件、这样就实现了不用修改现有代码就可以扩展和修改新的功能的一个目的。还是那个设计原则——open for extension, close for modification.
动态地将责任附加到对象上.若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
装饰者和被装饰者之间必须是一样的类型,也就是要有共同的超类。在这里应用继承并不是实现方法的复制,而是实现类型的匹配。因为装饰者和被装饰者是同一个类型,因此装饰者可以取代被装饰者,这样就使被装饰者拥有了装饰者独有的行为。根据装饰者模式的理念,我们可以在任何时候,实现新的装饰者增加新的行为。如果是用继承,每当需要增加新的行为时,就要修改原程序了。
尽量使功能独立拆分解耦,每种新的功能分离开来称为一个装饰者,当需要的时候,就可以组合在一起使用,而不是像单独使用继承那样将多个功能耦合在一起,阅读和使用不方便,并且不利用扩展,比如 有接口A 有功能1 2 3 4 5 如果单单使用继承,那么为了使结构符合面向对象编程,将会组合成10个子类,当功能进一步扩展的时候,数量时恐怖的,并且将是很难被使用者记忆和理解的。当我们使用了装饰者模式之后,仅仅需要实现数个装饰者,然后根据需要进行组合使用就可以了。
包装类不适合用在回调框架(callback framework)中,会出现SELF问题。
在回调框架中,对象把自身的引用传递给其他对象,用于后续的调用(回调)
SELF问题:被包装的对象并不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时却避开了外面的包装对象。
简而言之,继承的功能非常强大,但也存在诸多问题,因为违背了封装原则。只有当子类和超类确实存在子类型关系时,使用继承才是恰当的,但如果子类和超类在不同包中,并且超类并不是为了继承而设计的,那么继承会导致脆弱性,为了避免这种脆弱性,可以用符合和转发机制来代替继承,尤其是当存在适当的接口实现包装类的时候。包装类不仅比子类更加健壮,而且功能更加强大。
如何从继承和复合之间做出选择?
比较抽象的说法是,只有子类和父类确实存在"is-a"关系的时候使用继承,否则使用复合。
或者比较实际点的说法是,如果子类只需要实现超类的部分行为,则考虑使用复合。