面向对象的六大原则(三)

里氏替换原则的定义

里氏替换原则的英文为Liskov Substitution Principle,简称是LSP。里氏替换原则有两种定义:

  • 第一种定义 :
    If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.(如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。)

  • 第二种定义 :
    Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。)

第二种定义更加清晰明确,简单的说,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

问题由来

有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

经典范例

在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。下面看设计图,在此例中,正方形继承了长方形,这在我们眼中看起来好像很正常
ShowImage

代码实现如下

长方形Rectangle类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Rectangle {
private int mWidth;
private int mLength;
public void setLength(int length) {
mLength = length;
}
public void setWidth(int width) {
mWidth = width;
}
public int getLength() {
return mLength;
}
public int getWidth() {
return mWidth;
}
public int getArea() {
return mLength * mWidth;
}
}

正方形Square类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setLength(width);
}
@Override
public void setLength(int length) {
super.setLength(length);
super.setWidth(length);
}
}

运行测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LspTest {
//第一个案例 长方形返回的面积是50,正确
@Test
public void test1() {
Rectangle rectangle1 = new Rectangle();
rectangle1.setWidth(5);
rectangle1.setLength(10);
int area1 = rectangle1.getArea();
assertEquals(50, area1);
}
//第二个案例 正方形返回的面积是100,不正确
@Test
public void test2() {
Rectangle rectangle2 = new Square();
rectangle2.setWidth(5);
rectangle2.setLength(10);
int area2 = rectangle2.getArea();
assertEquals(50, area2);
}
}

运行结果
test1测试跑绿灯,test2测试不过,错误信息如下

1
2
3
java.lang.AssertionError:
Expected :50
Actual :100

发现在test2测试中,我们所期望的是,正方形就是长方形,那么正方形期望的面积也就理所应当的是50,然而在setWidth和setLength方法中,我们把父类Rectangle用子类Square进行替换,程序逻辑就不正常了,显然这就违背了里氏替换原则。因此,Rectangle类和Square类不应当是继承的关系。
仔细分析发现,正方形在设置长度和宽度这两个行为上,与长方形显然是不同的。长方形的行为:设置长方形的长度的时候,它的宽度保持不变,设置宽度的时候,长度保持不变。正方形的行为:设置正方形的长度的时候,宽度随之改变;设置宽度的时候,长度随之改变。所以,如果我们把这种行为加到基类长方形的时候,就导致了正方形无法继承这种行为。我们“强行”把正方形从长方形继承过来,就造成无法达到预期的结果。

里氏替换原则的启示

采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑。

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

参考代码

------ 本文结束 ------

版权声明


BillyYccc's blog by Billy Yuan is licensed under a Creative Commons BY-NC-SA 4.0 International License.
本文原创于BillyYccc's Blog,转载请注明原作者及出处!