Liskov替换原则(The Liskov Substitution Principle)
子类型(subtype)必须能够替换掉它们的基类型(base type)。
一个违反LSP的简单例子
public class AntiLspDemo {
public void drawShape(final Shape shape) {
if (shape.getItsType() == ShapeType.Square) {
final Square square = (Square) shape;
square.draw();
} else if (shape.getItsType() == ShapeType.Circel) {
final Circle circle = (Circle) shape;
circle.draw();
}
}
}
enum ShapeType {
/**
* 正方形.
*/
Square,
/**
* 圆形.
*/
Circel
}
class Point {
private double x;
private double y;
// get/set...
}
class Shape {
private ShapeType itsType;
/**
* 获取itsType.
* @return the itsType
*/
public ShapeType getItsType() {
return itsType;
}
/**
* 设置itsType.
* @param newItsType the itsType to set
*/
public void setItsType(ShapeType newItsType) {
itsType = newItsType;
}
}
class Circle extends Shape {
private Point itsCenter;
private double itsRadius;
public Circle() {
this.setItsType(ShapeType.Circel);
}
public void draw() {
System.out.println("绘制Circle...");
}
}
class Square extends Shape {
private Point itsTopLeft;
private double itsSide;
public Square() {
this.setItsType(ShapeType.Square);
}
public void draw() {
System.out.println("绘制Square...");
}
}
正方形和矩形,更微妙的违规
我们经常说继承是IS-A(“是一个”)关系。如果一个新类型的对象被认为和一个已有类的对象之间满足IS-A关系,那么这个新对象的类应该从这个已用对象的类派生。
一个正方形是一个矩形,所以Square类就应该派生在Rectangle类。不过,这将带来一些微妙但极为值得重视的问题。
public class Rectangle {
/**
* 左上角坐标.
*/
private Point topLeft;
/**
* 宽度.
*/
private double width;
/**
* 高度.
*/
private double height;
/**
* 获取topLeft.
* @return the topLeft
*/
public Point getTopLeft() {
return topLeft;
}
/**
* 设置topLeft.
* @param newTopLeft the topLeft to set
*/
public void setTopLeft(Point newTopLeft) {
topLeft = newTopLeft;
}
/**
* 获取width.
* @return the width
*/
public double getWidth() {
return width;
}
/**
* 设置width.
* @param newWidth the width to set
*/
public void setWidth(double newWidth) {
width = newWidth;
}
/**
* 获取height.
* @return the height
*/
public double getHeight() {
return height;
}
/**
* 设置height.
* @param newHeight the height to set
*/
public void setHeight(double newHeight) {
height = newHeight;
}
}
public class Square extends Rectangle {
public void setWidth(double width) {
super.setWidth(width);
super.setHeight(width);
}
public void setHeight(double height) {
super.setWidth(height);
super.setHeight(height);
}
}
真正的问题
Square类没有违反正方形的不变性,但是Square派生自Rectangle,Square类违反了Rectangle类的不变性。
public void g(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert(r.area() == 20)
}
有效性并非本质属性
一个模型,如果孤立地看,并不具有真正意义上的有效性。模型的有效性只能通过它的Client程序来体现。这又是一个实践TDD的好理由。
IS-A是关于行为的
对于那些不是g的调用者而言,正方形可以是长方形,但是从g的角度,Square对象绝对不是Rectangle对象。Square对象的行为方式和函数g所期望的Rectangle对象的行为方式不相容。从行为方式的角度来看,Square不是Rectangle,对象的行为方式才是软件真正所关注的。
基于契约设计
基于契约设计(Design By Contract),类的编写者显示地规定针对该类的契约。契约是通过为每个方法声明的前置条件(preconditions)和后置条件(postconditions)来指定的。要执行一个方法,前置条件必须为真。执行完毕后,保证后置条件为真。
Rectangle.setWidth(double w)的后置条件:
assert((width == w) && (height == old.height));
派生类的前置条件和后置条件规则是:
在重新声明派生类中的例程(routine)时,
只能使用相等或更弱的前置条件,只能使用相等或更强的后置条件。