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