liskov替换原则
这是有关SOLID软件原理的系列文章中的第三篇。 我们之前介绍了“ 单一责任原则”和“ 开放-封闭原则” 。 在这篇文章中,我将带您了解SOLID中的L,即Liskov替代原理。
开闭原则背后的主要思想是通过继承实现的,即为新功能引入新的类,并使与现有功能相关的类保持不变。 但是,好的继承结构和坏的继承结构又有什么区别呢? 这就是里斯科夫替代原则发挥作用的地方。
Liskov替换原理是一种改进代码的简单而有效的方法。 但是,识别代码何时违反原理并不是那么简单。 通常,破坏Liskov替换原理的一段代码也破坏了Open-Close原理。
里斯科夫替代原则
最初由Barbara Liskov撰写,其中指出:
如果对于类型S的每个对象o1,都存在类型T的对象o2,使得对于用T定义的所有程序P,用o1代替o2时P的行为不变,则S是T的子类型
自原始论文以来,出现了更简单的定义,它们的定义为:
如果S是T的子类型,则可以用类型S的对象替换类型T的对象,而无需更改程序的任何所需属性。
这听起来很简单,但不只是眼神。 此外,从基类继承时,每个派生类还应遵守许多要求。
签名要求
将通过一个示例说明有关继承的签名要求。 假设存在三个类,这样关系的方向为Vehicle-> Car-> Ford,即Vehicle是Car的超类型,而Car是Ford的基类。
子类型中方法参数的矛盾
按照此规则,方差必须与继承关系的方向相反,即,子类型S
方法中的每个输入参数必须与基类T中的相应输入参数相同或超类型 。
//Supertype
void drive(Car v);
//Invalid subtype
void drive(Ford f);
//Valid subtype
void drive(Vehicle v);
//or
void drive(Car c);
子类型中返回类型的协方差
这意味着方差必须沿继承关系的方向进行,即子类型S
的每种方法的输出必须相同或基类T
相应输出参数的子类型 。
//Supertype
Car getInstance();
//Invalid subtype
Vehicle getInstance();
//Valid subtype
Car getInstance();
//or
Ford getInstance();
没有新的例外
子类型的方法不应抛出新的异常,除非这些异常本身是基类的方法所抛出的异常的子类型。
//Supertype
Car getInstance() throws CarNotFoundException;
//Invalid subtype
Vehicle getInstance() throws VehicleNotFoundException;
//Valid subtype
Car getInstance() throws CarNotFoundException;
//or
Ford getInstance() throws FordNotFoundException;
签名要求通常很容易识别,因为在大多数静态类型的现代语言中,编译器会强制执行类型安全性并指出错误。 行为要求很难识别。
接下来将介绍继承必须满足的行为条件。
行为要求
超类型的不变量必须保留在子类型中
不变性是在程序执行期间可以依赖为真的条件。 在子类型的实现中,不变性应保持不变。
前提条件不能在子类型中得到加强
前提条件是在执行某些代码段之前必须始终为真的条件或谓词。 前提条件可以在子类型中进行修改,但只能被削弱,而不能被增强。
子条件不能弱化后置条件
后置条件是在执行某些代码段之后必须始终为真的条件或谓词。 后置条件可以在子类型中进行修改,但只能增强而不是减弱。
让我们通过一个常用示例来了解它们的含义。
例
从数学上讲,正方形是矩形。 注意单词“ 是 ”。 但是,在编码时,正方形实际上是否是两边相等的矩形? 大多数人会说是,他们会误解“ 是 ”关系,并通过继承对矩形和正方形之间的关系进行建模。
在这种情况下,您应该可以在任何可以使用Rectangle类的地方使用Square,不是吗? 但是这样做会导致一些意想不到的问题,并破坏了李斯科夫替代原则。 让我们检查为什么?
让我们按照内联注释中提到的行为要求如下定义Rectangle类。
/* The rectangle class and its behavioural conditions */
public class Rectangle {
/*
* Invarients:
* Height and width cannot change together in the same method i.e.
* setHeight() should not change width
* setWidth() should not change height
*/
private int height;
private int width;
/*
* Post condition:
* height==newHeight && width==oldwidth
*
* Pre condition:
* The rectangle is not a rhombus, if it is, then it is a square.
* The setHeight should only execute on a rectangle.
*/
public void setHeight(int newHeight) {
this.height = newHeight;
}
/*
* Post condition:
* width==newWidth && height==oldHeight
*
* Pre condition:
* The rectangle is not a rhombus, if it is, then it is a square.
* The setWidth should only execute on a rectangle.
*/
public void setWidth(int newWidth) {
this.width = newWidth;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}
在第一次尝试中,使用继承将Rectangle扩展为Square可能会导致以下结果:
public class Square extends Rectangle {
@Override
public void setHeight(int height) {
super.setHeight(height);
}
@Override
public void setWidth(int width) {
super.setWidth(width);
}
}
但是,嘿,不是吗? 您可能最终会在侧面具有两个不同的尺寸-并且当发生这种情况时,它肯定不是正方形。 易于修复。 大多数人会这样做。
public class Square extends Rectangle {
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
}
上面的代码对所有行为规则均未采用《李斯科夫替代原理》。 也就是说,当我们使用Rectangle类型传递Square的实例时。
- 根据基类Rectangle中指定的不变规则,不允许在同一方法中设置两个尺寸。 因此,关于不变的行为规则将失败。 仅此一个充分的理由就不通过继承Rectangle来建模Square。
- 对于setHeight和setWidth方法,在基类Rectangle中定义的后置条件将失败。 由于我们在setWidth()和setHeight()中都同时修改了高度和宽度,因此旧值的检查将失败。 我们无法删除后置条件来检查旧值,因为它会削弱后置条件。
- 前提条件将失败,因为仅当对象不是菱形时才允许对基类进行操作。 由于正方形是菱形,而矩形不是菱形,所以将Square对象传递给Rectangle类时,存在于Rectangle类中的前提条件将不会执行该代码。 如果它是正方形,我们不能添加任何条件来允许该操作,因为它将增强先决条件。
那么,如何正确地建模继承呢?
继承最关键的方面是我们应该基于行为而不是对象属性对继承进行建模。
通常的趋势是基于现实世界的对象属性在代码中为对象建模。 但这是不正确的,因为代码中的对象不是真实的对象,它们只是真实对象的表示 。
例如,前面示例中的矩形和正方形对象本身不是矩形和正方形,它们只是表示这些形状。 表示不必总是与它们表示的真实对象共享相同的关系 。 再举一个例子,当我和妻子一起玩视频游戏时,我们两个人分别由各自的xbox化身来代表 ,但是化身本身并没有彼此结婚!
翻译自: https://www.javacodegeeks.com/2016/04/solid-liskov-substitution-principle-2.html
liskov替换原则