前言
相信学过一些OO的人都对几个基本的面向对象设计原则印象深刻,比如面向抽象、liskov、开闭原则等。这里重点讨论一下composition over inheritance的思想和jdk本身设计中的几个反例。希望通过这些分析能够有一个更深刻的理解而不至于浮于表面。
概念及对比
关于composition以及inheritance的概念其实再简单不过了。我们在创建的某个类中包含对其他类的引用,在实现某些功能的时候实际上是利用到了被引用类的一些方法或者功能。从相互关系来看,这相当于是一个很简单的直接引用。这就是composition。
而inheritance在我们刚开始学习某种OO的语言时都会提到。一个类如果继承了另外一个类,则它可以重用父类里面的方法和属性。从使用的角度来说,这个子类必须是这个父类的某一个特殊的类型,他们是一种is a的关系。比如说我们定义animal这么一个类,那么如果我们要定义猫或者狗的类是,可以继承这个animal,因为猫和狗都是一种animal。
既然我们已经理解了这些基本的概念之后,在哪些情况下使用composition合适而在什么时候使用inheritance合适呢?虽然从原来OO的设计原则来看是应该优先考虑composition,那么是否就应该盲目的一律用composition呢?
对于这两种情况的选择,我们可以在针对问题场景的时候先如此考虑。我们新的类和原有类是否为is a的关系呢?如果是的话,则意味着应用于父类的所有方法以及属性必然适用于子类。比如说我们有一个类Person:
public class Person {
private String name;
private String id;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void printName() {
System.out.println(name);
}
}
如果我们要定义一个新的Employee的类,它本身也有name, id。而且也有显示name信息的方法要求。这个时候,我们发现如果继承Person的话,父类里的属性可以被Employee所拥有, 关键是父类里所有的方法对于子类来说都是适用的。那么这个Employee的类可以定义成如下:
public class Employee extends Person {
private String title;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
这里我们看到最重要的一点是Employee继承了Person了之后,可以在它本身调用父类的getName, setName以及printName这些方面。它本身还增加了title相关的方法。这里实现了一个理想的重用。
现在,我们再换个思路来看他们之间的关系,如果我们用composition的方式来实现Employee,则该如何呢?首先我们要看到,Employee里面也需要name, id。那么他们相关的方法在Employee里面也需要定义。那么具体的实现代码则应该如下:
public class Employee {
private Person person;
private String title;
public Employee(Person person) {
this.person = person;
}
public void setName(String name) {
this.person.setName(name);
}
public String getname() {
return person.getName();
}
public void printName() {
person.printName();
}
public void setTitle(String title) {
this.title = title;
}
// ... omitted
}
这里的代码也实现了同样的功能,和前面的比较起来需要一个额外的以Person为参数的构造函数。而且,最重要的就是需要额外写一遍和Person里定义的同样的方法,但是因为可以直接用Person里的方法,就直接调用Person里的。这样就额外的写了一些delegate的代码。所以,我们可以看到,虽然composition也可以实现inheritance的这些功能,在满足inheritance的条件下,有一大堆的delegate的代码要写。这样就平添了很多无意义的代码。
所以我们在比较这两种情形的取舍时,可以先看看新的类是否可以完全适用于父类的所有场景,如果可以使用inheritance,否则应该考虑composition。这里举的示例是一个简单的情形。下面我们再看一些实际项目中使用inheritance不当的地方。
实例
这里的一些示例取自jdk里面的代码。有些由于版本和当初设计的考虑,忽略了这么一些问题,因此会存在一些使用的隐患,而且影响到后续的使用以及修正。
Properties vs HashTable
java.util.Properties,这个类在java里面算是比较常用的。主要用来保存我们读取的配置文件信息。在很多应用里,我们需要设置一些环境相关的值,针对不同的执行环境以及要求进行修改。这个时候我们一般会设置一个xxx.properties的文件。文件名可以随意设定,然后在读取文件和解析的程序里将文件名传入。(关于properties文件的读取可以参考这篇文章)。关键是,我们读取到的内容就保存在一个Properties类型的对象里。我们可以通过getProperty(String key)来读取特定的配置属性值。我们这里应用的时候,有一个很重要的设定就是,我们所要读写访问的属性都是String类型的。
这里怎么会扯上HashTable呢?(HashTable详细实现分析可以参考这篇文章)因为我们Properties里面访问的本质上就是一组组的名值对,而且在jdk里Properties的实现就是通过继承HashTable做的。
如下是部分Properties的代码:
public class Properties extends Hashtable<Object,Object> {
private static final long serialVersionUID = 4112578634029874840L;
protected Properties defaults;
public Properties() {
this(null);
}
public Properties(Properties defaults) {
this.defaults = defaults;
}
// ...omitted
}
这里Properties的设计为什么不太合理呢?从前面我们的讨论来看,我们所有应用于HashTable的场景都适用于Properties吗?不一定吧?首先一个,HashTable里可以支持的参数不仅限于String类型,而Properties只能使用String类型。如果我们定义一个Properties的对象,然后我们就可以使用HashTable里的get, put等方法。而且往里面传入一些非String类型的参数。反正Properties继承的是HashTable<object, object>。只要是对象就行。这些方法对于Properties来说其实是没有意义的。如果我们故意使坏,利用put方法往里面放一些非String类型的对象,然后再用Properties的getProperties方法去取。在Properties的getProperty方法里是使用了父类HashTable的get方法,其具体实现如下:
public String getProperty(String key) {
Object oval = super.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
}
在后面的读取中虽然不会取到我们乱加入的对象,但是可能会存在要取的String的hash值和我们加入的对象的hash值相同。可能会导致取的时候带来额外的比较,降低执行效率。
Stack vs Vector
我们再来看看Stack和Vector。(关于Stack和Vector的详细实现讨论可以见此文章)从我们的理解来看,其实Stack和Vector之间并不是一个is a的关系。首先一个,我们理解的Stack是一个先入后出的结构。也就是说,我们要添加一个元素到Stack里,只能通过push操作,而要移除某个元素的时候,需要等它上面的元素都pop出去了才能操作。而Vector是什么呢?本质上就相当于一个线程安全的可变长数组。对于一个数组我们可以随机的操作其中的元素。但是如果对Stack这么来的话就有点不太合适了。
通过前面这两个jdk里的实现我们看到了,在这种使用composition的情况下,我们只是部分的需要使用到其他类的特性。如果直接去继承的话会带来不必要的麻烦。
总结
继承有风险,用时需谨慎。这就是为什么在OO设计原则里会这么提。所以我们在考虑一些对象之间的关系时,可以这样来看。继承就相当于你做了某个人家的孩子,那么你就要继承他们给定的姓氏以及财富等。对于他们所有的这些限制对于你是必须有效的。如果对于父类的所有特性对于子类不能适用,那就要出大事了。而对于composition来说,则好比是你的一个好朋友。你需要人家的一部分帮助或者特性,但不是全盘照搬。虽然有很多事都可以赖人家去做,自己只要说一声就可以。
参考材料