动机
当我们设计程序模块时,我们会创建一些类层次结构,然后我们通过扩展一些类来创建它们的子类。
我们必须确保子类只是扩展而没有替换父类的功能,否则当我们在已有程序模块中使用它们时将会产生不可预料的结果。
里氏代换原则表明当一个程序模块使用基类时,基类的引用可以被子类替换而不影响模块的功能。
里氏代换原则
基类完全能够被子类替代而不影响模块的功能。
实例
对于多态来说里氏代换原则好像是很显然的事情,例如:
- public void drawShape(Shape s) {
- // Code here.
- }
public void drawShape(Shape s) {
// Code here.
}
对于Shape的任何子类来说,drawShape方法都应该能很好的工作。我们必须小心的实现子类以免无意中违反了里氏代换原则,如果一个函数不满足里氏代换原则,那么它可能必须显式地引用子类对象,这样的函数同样违反了开闭原则,因为当添加新的子类时,必须修改它。
考虑下面的矩形类:
- // A very nice Rectangle class.
- public class Rectangle {
- private double width;
- private double height;
- public Rectangle(double w, double h) {
- width = w;
- height = h;
- }
- public double getWidth() {return width;}
- public double getHeight() {return height;}
- public void setWidth(double w) {width = w;}
- public void setHeight(double h) {height = h;}
- public double area() {return (width * height);
- }
// A very nice Rectangle class.
public class Rectangle {
private double width;
private double height;
public Rectangle(double w, double h) {
width = w;
height = h;
}
public double getWidth() {return width;}
public double getHeight() {return height;}
public void setWidth(double w) {width = w;}
public void setHeight(double h) {height = h;}
public double area() {return (width * height);
}
现在,如果有个正方形呢?显然正方形是一个矩形,所以我们应该让正方形继承矩形类,是这样吗?我们看一下!
注意:
- 正方形不需要同时具有宽和高属性,但是它还是从矩形继承了这些属性。所以,每个正方形都浪费了一点空间,但这不是我们关注的主要问题。
- 继承而来的setWidth()和setHeight()方法实际上对于正方形是不合适的,因为正方形的宽和高是相等的。所以我们需要重写setWidth()和setHeight()方法,这可能暗示着在这儿并不适合使用继承。
下面是Square类:
- // A Square class.
- public class Square extends Rectangle {
- public Square(double s) {super(s, s);}
- public void setWidth(double w) {
- super.setWidth(w);
- super.setHeight(w);
- }
- public void setHeight(double h) {
- super.setHeight(h);
- super.setWidth(h);
- }
- }
// A Square class.
public class Square extends Rectangle {
public Square(double s) {super(s, s);}
public void setWidth(double w) {
super.setWidth(w);
super.setHeight(w);
}
public void setHeight(double h) {
super.setHeight(h);
super.setWidth(h);
}
}
一切看上去都很好,但是注意下面的代码:
- public class TestRectangle {
- // Define a method that takes a Rectangle reference.
- public static void testLSP(Rectangle r) {
- r.setWidth(4.0);
- r.setHeight(5.0);
- System.out.println("Width is 4.0 and Height is 5.0" +
- ", so Area is " + r.area());
- if (r.area() == 20.0)
- System.out.println("Looking good!\n");
- else
- System.out.println("Huh?? What kind of rectangle is
- this??\n");
- }
- public static void main(String args[]) {
- // Create a Rectangle and a Square
- Rectangle r = new Rectangle(1.0, 1.0);
- Square s = new Square(1.0);
- // Now call the method above. According to the
- // LSP, it should work for either Rectangles or
- // Squares. Does it??
- testLSP(r);
- testLSP(s);
- }
- }
public class TestRectangle {
// Define a method that takes a Rectangle reference.
public static void testLSP(Rectangle r) {
r.setWidth(4.0);
r.setHeight(5.0);
System.out.println("Width is 4.0 and Height is 5.0" +
", so Area is " + r.area());
if (r.area() == 20.0)
System.out.println("Looking good!\n");
else
System.out.println("Huh?? What kind of rectangle is
this??\n");
}
public static void main(String args[]) {
// Create a Rectangle and a Square
Rectangle r = new Rectangle(1.0, 1.0);
Square s = new Square(1.0);
// Now call the method above. According to the
// LSP, it should work for either Rectangles or
// Squares. Does it??
testLSP(r);
testLSP(s);
}
}
测试程序输出:
- Width is 4.0 and Height is 5.0, so Area is 20.0
- Looking good!
- Width is 4.0 and Height is 5.0, so Area is 25.0
- Huh?? What kind of rectangle is this??
Width is 4.0 and Height is 5.0, so Area is 20.0
Looking good!
Width is 4.0 and Height is 5.0, so Area is 25.0
Huh?? What kind of rectangle is this??
看起来我们违反了里氏代换原则,问题在哪儿?testLSP()方法合理的假设当一个矩形的宽改变时,它的高度不变。当传递一个正方形对象时,该方法却违反了里氏代换原则。从数学上看,正方形是一个矩形,但是一个正方形对象却不是矩形对象,因为一个正方形对象的行为和一个矩形对象的行为不一致。从行为上来说,正方形不是矩形!里氏代换原则清晰的说明,IS-A关系是对于所有的行为来说的,为了遵循里氏代换原则,子类的行为必须和客户端使用的基类的行为一致。
子类不能比基类具有更多的约束,因为必须在任何可以使用基类的地方使用子类,如果子类比基类有更多的约束,那么就会出现基类可用,但却违反了子类约束的情况。
总结
里氏代换原则是对开闭原则的扩展,它表明我们在创建基类的新的子类时,不应该改变基类的行为。