面向对象理论诞生之初,由于没有最佳实践的指导,往往导致继承的滥用。一个很著名的例子就是java类库中的堆栈类Stack继承自向量类Vector。
public class Stack extends Vector ...{ public Object push(Object item) ...{ addElement(item); return item; } public synchronized Object pop() ...{ //... }}仅仅是为了重用Vector的管理元素的方法,就让Stack继承自Vector,导致了一个蹩脚的设计。人们后来总结出一条规律:优先使用组合而不是继承,只有当类A确实"is a"类B的时候,才使用继承。Stack不是Vector,所以这个时候应该使用组合。示例代码如下:
public class Stack...{ private Vector vector; public Object push(Object item) ...{ vector.addElement(item); return item; }//...}只有符合"is a"关系时才能使用继承,这条规律是符合我们的直觉的。面向对象技术就是用软件里面的对象模拟现实中的对象。如果狗不是猫,当然不能让狗继承自猫。但是实际的软件开发中,人们很快发现有些情况下即使类A确实"is a"类B,也不能使用继承。最著名的一个例子就是正方形不能继承自长方形了,很多讲面向对象设计的书里都有。这是一个明显违反直觉的例子,示例代码如下:
public class Rectangle ...{ private double width; private double height; public Rectangle(double width,double height)...{ this.width=width;this.height=height; } public double getHeight() ...{ return height; } public void setHeight(double height) ...{ this.height = height; } public double getWidth() ...{ return width; } public void setWidth(double width) ...{ this.width = width; }//...}public class Square extends Rectangle...{ public void setHeight(double height) ...{ super.setHeight(height); super.setWidth(height); } public void setWidth(double width) ...{ super.setHeight(width); super.setWidth(width); }//...}
因为Rectangle类中的setHeight和setWidth方法对Square类不合适,为了保证正方形的长和宽相等,改写了这两个方法。上面的代码初看上去很合理,其实有问题。这个代码的缺陷我就不详细讨论了,很多书都对其做了详细的分析。我只是从理论上指出其不合理之处。根据契约式设计,有下面的原则:
子类的前置条件不能强于父类的前置条件。 子类的后置条件不能弱于父类的后置条件。 子类的类不变式不能弱于父类的类不变式。这个原则也可以由Liskov替换原则得来。比方说要能够把父类替换成子类,那么父类所接受的任何参数,子类也必须能接受,也就是子类的前置条件不能强于父类的前置条件。
对于长方形类的setHeight方法,其前置条件是height>0,其后置条件是this.height==height&&this.width==old.width。对于正方形类的setHeight方法,其前置条件也是height>0,不强于父类的前置条件。但是其后置条件变成this.height==height&&this.width==height。它违背了父类的后置条件。子类的后置条件不能弱于父类的后置条件的意思是子类必需遵守父类的后置条件,同时还可以加上自己的更强的后置条件。
对于上面这种反常的情况,人们提出了子类型的概念。上面的代码中Square类是Rectangle类的子类(subclass),但是不是子类型(subtype)。因为它违反了Liskov替换原则。这个概念的引入应该说是有很大的混淆性。
那么到底为什么会出现这种反常的情况,正方形就是长方形,使用继承是合情合理的。如果一个设计方法会出现太多的例外情况,那它肯定不是一种好的设计方法。
老师:同学们请看!这是一个正方形,现在我让它的垂直边高度增加,现在成为什么形状了?
学生:报告老师,是长方形。
老师:回答正确。
我们看到在现实中完全可以单独改变正方形的边长,这时改变以后的形状不再是正方形了。通过和现实情况对比,我们受到了启发。那就是正方形和长方形应该是不可变类。当它的边长改变以后,就变成了一个新的长方形。
public class Rectangle ...{ private double width; private double height; public Rectangle(double width,double height)...{ this.width=width;this.height=height; } public double getHeight() ...{ return height; } public double getWidth() ...{ return width; } public Rectangle changeHeight(double height) ...{ return new Rectangle(this.width,height); } public Rectangle changeWidth(double width) ...{ return new Rectangle(width,this.height); }}public class Square extends Rectangle ...{ public Square(double dege) ...{ super(dege, dege); } public Square changeEdge(double edge)...{ return new Square(edge); } }通过这种思路,我们就得到了一个完全和现实对象相吻合的设计。所以说并不是正方形不能继承自长方形。