×

消息

EU e-Privacy Directive

This website uses cookies to manage authentication, navigation, and other functions. By using our website, you agree that we can place these types of cookies on your device.

View e-Privacy Directive Documents

You have declined cookies. This decision can be reversed.

相对于继承(inheritance),应更优先使用组合(composition)

继承是一种非常强大的代码重用方式,但它并不总是最好的方式。

在同一个软件包内可以安全地使用继承,这是因为父类和子类都在同一群开发者的维护之下。或者父类是被设计为专用于继承,那么同样可以安全地使用。相反,跨软件包去继承普通的类,则可能是危险的。

不同于方法调用,继承会破坏类的封装。换言之,子类必须依赖于父类的实现细节才能正常运行,而父类的实现完全可能在不同版本之间发生变化,这会破坏子类代码。

比如下面的代码。继承了HashSet类,增加一个计数器,以统计Set中曾加入过的元素总数:

// Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
	// The number of attempted element insertions
	private int addCount = 0;
	
	public InstrumentedHashSet() {
	}
	
	public InstrumentedHashSet(int initCap, float loadFactor) {
		super(initCap, loadFactor);
	}
	
	@Override public boolean add(E e) {
		addCount++;
		return super.add(e);
	}
	@Override public boolean addAll(Collection<? extends E> c) {
		addCount += c.size();
		return super.addAll(c);
	}
	public int getAddCount() {
		return addCount;
	}
}

看上去很好,但这段代码所实现的计数功能无法正确运行。比如通过addAll方法加入三个元素:

InstrumentedHashSet<String> s = 
	new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));

预期的结果是3,实际上getAddCount方法却将返回6。这是因为子类覆盖的addAll方法调用了super.addAll,而在父类HashSet中,addAll是通过反复调用add来实现的。所以当子类调用addAll方法时,它本身为计数器增计3次,之后super.addAll通过被子类覆盖的add方法,又增计3次,最后得到了错误结果6。这段代码当然可以修复,但这种修复将或多或少依赖于现有父类HashSet的实现,如果在未来某个版本中,HashSet的行为有所改变,这些修复代码仍然有可能损坏。

为避免继承可能引入的问题,可以使用"组合"这种设计。所谓组合,就是创建一个新的类,并把要重用的已有类的对象作为新类的一个属性。并且新对象可以通过调用其成员对象的方法来实现原有类的功能,这种架构被称为"转发"(forwarding)。相应的,新类中的方法被称为"转发方法"(forwarding methods)。下面的代码是组合版本的"可计数集合":

// Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E> {
	private int addCount = 0;
	
	public InstrumentedSet(Set<E> s) {
		super(s);
	}
	
	@Override public boolean add(E e) {
		addCount++;
		return super.add(e);
	}
	
	@Override public boolean addAll(Collection<? extends E> c) {
		addCount += c.size();
		return super.addAll(c);
	}
	
	public int getAddCount() {
		return addCount;
	}
}
// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
	private final Set<E> s;
	
	public ForwardingSet(Set<E> s) { this.s = s; }
	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 add(E e) { return s.add(e); }
	public boolean remove(Object o) { return s.remove(o); }
	
	public boolean containsAll(Collection<?> c)
		{ return s.containsAll(c); }
	public boolean addAll(Collection<? extends E> c)
		{ return s.addAll(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(); }
}

这样的设计,相对于继承架构,不仅更可靠,也更加灵活。它可以用于Set接口的所有实现,而不必依赖于某个具体的类或构造函数:

Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(cmp));
Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));

"组合"设计还有另外好几个名字,类似上面例子中InstrumentedSet那样的类还经常被称为"包装类"(wrapper class),这种架构实际上也是decorator设计模式。不严格地说,组合与转发同时出现时,还可以被称为"代理"(delegation)。但严格说来,"代理"要求包装对象(wrapper object)传递一个指向自身的引用到被包装的对象。

上述wrapper架构少有缺点,首要的瑕疵就是它不适用于"回调"(callback)框架。由于被包装的对象经常无法获得其包装者的信息,所以当它把自身引用传递给其他对象作为回调时,其包装者将在回调过程中被绕过。

继承架构真正适用的场景其实只有一个,那就是当父类与子类有真正逻辑上的同质关系时。比如B类继承了A类,那么要求任何一个B都真的"是"一个A。Java标准库中有很多违背这一原则的反例,比如Stack继承了Vector,Properties继承了Hashtable。这些错误都在现实中带来了很多问题,所以开发者应该尽可能避免这样的设计缺陷。

提交评论


安全码
刷新