SOLID设计原则的之中的开闭原则(Open/Closed Principle, OCP)主要是基于抽象和多态实现的。而实现抽象和多态的关键机制之一就是继承。
如何设计继承体系才能使得抽象和多态正常的发挥作用,并且不违背开闭原则呢? 这是里氏替换原则(Liskov Substitution Principle, LSP)要解决的问题。
里氏替换原则的定义如下:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
即: 方法(函数)中使用对基类对象引用的地方,必须可以替换成其子类对象,而方法并不知道发生了这一替换。
自己关于without knowing it
的理解:
里氏替换原则并没有要求子类不能重写父类方法,有些博客中这样说应该是错误的,参考:
https://stackoverflow.com/questions/1735137/liskov-substitution-principle-no-overriding-virtual-methods
上面的定义有些难以理解,下面结合例子进一步说明:
违反里氏替换原则的一个典型的例子就是试图在运行时期判断对象的实际类型,例如下面的代码中App
类的drawShape()
方法:
class Point {
private double x;
private double y;
}
class Shape { }
class Circle extends Shape {
private Point center; // 圆心
private double radius; // 半径
public Circle(Point center, double radius) {
this.center = center;
this.radius = radius;
}
public void draw() {
// draw circle
}
}
class Square extends Shape {
private Point topLeft; // 左上角点
private double sideLen; // 边长
public Square(Point topLeft, double sideLen) {
this.topLeft = topLeft;
this.sideLen = sideLen;
}
public void draw() {
// draw square
}
}
public class App {
public void drawShape(Shape shape) {
if (shape instanceof Circle)
((Circle) shape).draw();
else if (shape instanceof Square)
((Square) shape).draw();
}
}
你或许会觉得,drawShape()
方法中的shape
用子类Circle
或Square
的对象替换不是完全可以吗,程序还是正常运行? 为什么违背里氏替换原则了?
注意前面提到定义中的without knowing it
,drawShape()
的参数替换为子类Circle
或Square
的对象后确实能够正常工作,但是它是建立在了解子类的基础之上的,也就是我们提前知道了drawShape()
会接受子类对象, 显然违背了里氏替换原则。
进一步理解,里氏替换原则的目的是为了规范继承体系,使得多态能够正常工作,不违背开闭原则。而上面的代码显然违背了开闭原则,因为每增加一个新的Shape
的子类,就要修改drawShape()
的代码,增加一个新的else if
语句来判断新增的类型。
下面是对上面代码的改进,使之符合里氏替换原则:
class Point {
private double x;
private double y;
}
abstract class Shape {
public abstract void draw();
}
class Circle extends Shape {
private Point center; // 圆心
private double radius; // 半径
public Circle(Point center, double radius) {
this.center = center;
this.radius = radius;
}
@Override
public void draw() {
// draw circle
}
}
class Square extends Shape {
private Point topLeft; // 左上角点
private double sideLen; // 边长
public Square(Point topLeft, double sideLen) {
this.topLeft = topLeft;
this.sideLen = sideLen;
}
@Override
public void draw() {
// draw square
}
}
public class App {
public void drawShape(Shape shape) {
shape.draw();
}
}
正方形是特殊的长方形,它们之间存在IS-A
的关系。那么我们是不是可以让正方形继承长方形呢? 假设可以,看如下的代码:
class Rectangle {
private int width;
private int height;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
/**
* 确保长和宽同时被设置, 避免违反正方形的定义,下同
* @param width 宽度
*/
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}
public class App {
public void foo(Rectangle rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(4);
assert rectangle.getArea() == 20;
}
}
在App
类的foo()
方法中,我们基于长方形的性质(即我们自定义的预期): 面积 = 长 × 宽
断言了rectangle.getArea() == 20
。
而对foo()
方法,如果传入一个Square
类实例,断言就会报错。这显然违背了我们对foo()
方法的预期。
因此在foo()
方法中,子类Square
的对象不能替换超类Rectangle
的对象,说明这个继承关系违背了里氏替换原则。
上面的第二个例子中,我们对foo()
方法定义了一个预期(面积 = 长 × 宽
)。这个预期是我们脑子里的约定,对于上面的foo()
方法我们也可以有别的预期。
那么有没有一种方法,能将我们的预期写到代码里面,从而约束程序员按照这个预期来编程,防止违背里氏替换原则呢? 当然有了,可以借助**契约设计(Design by Contract)**的方法来实现。
契约设计涉及以下几个术语:
关于这三个属于,参考:
- https://stackoverflow.com/questions/35303332/what-are-preconditions-and-postconditions
- https://www.cs.cmu.edu/~ckaestne/15214/s2017/slides/20170131-design-for-reuse-1.pdf
我们可以在代码中指定precondition, postcondition, invariant
来对代码进行约束。
用契约设计的方法来看,要使得设计遵循里氏替换原则应该满足:
(个人理解)并且方法的实现不能和上面3个条件产生冲突
例子中的require代表前置条件,ensures代表后置条件
下面满足上面三个条件(不变条件更严格,前置条件放宽,后置条件更严格),是符合里氏替换原则的设计
class Car extends Vehicle {
int fuel;
boolean engineOn;
//@ invariant fuel >= 0;
//@ requires fuel > 0 && !engineOn;
//@ ensures engineOn;
void start() { …}
void accelerate() { …}
//@ requires speed != 0;
//@ ensures speed < old(speed)
void brake() { …}
}
class Hybrid extends Car {
int charge;
//@ invariant fuel >= 0 && charge >= 0;
//@ requires (charge > 0 || fuel > 0) &&!engineOn;
//@ ensures engineOn;
void start() { …}
void accelerate() { …}
//@ requires speed != 0;
//@ ensures speed < \old(speed)
//@ ensures charge > \old(charge)
void brake() { …}
}
下面例子中Rectangle
的setWidth()
方法破坏了Square
类的不变条件h == w
,因此不遵循里氏替换原则。
Java编译器内置的一些规则遵循了里氏替换原则: