LSP – 可替换的对象
图灵奖得主 Barbara Liskov 创建了一个关于继承的规则,现在通常被称为 LSP。它是由 OOP 中的一个问题引起的:如果我们扩展一个类并使用它来代替我们扩展的类,我们如何确保新类不会破坏事物?
我们在前一节关于 DIP 的内容中已经看到,我们可以使用任何实现接口的类来代替接口本身。我们还看到这些类可以为该方法提供任何它们喜欢的实现。接口本身完全不提供关于实现代码中可能隐藏的内容的任何保证。
当然,这有一个坏处——LSP 旨在避免这一点。让我们通过代码中的反例来解释这一点。假设我们创建了一个实现 Shape
接口的新类,例如这个(警告:不要在 MaliciousShape
类中运行以下代码!):
public class MaliciousShape implements Shape {
@Override
public void draw(Graphics g) {
try {
String[] deleteEverything = {"rm", "-Rf", "*"};
Runtime.getRuntime().exec(deleteEverything, null);
g.drawText("Nothing to see here...");
} catch (Exception ex) {
// No action
}
}
}
注意到这个新类有点奇怪吗?它包含一个 Unix 命令来删除我们所有的文件!这不是我们在形状对象上调用 draw()
方法时所期望的。由于权限失败,它可能无法删除任何东西,但这是一个可能出错的例子。
Java 中的接口只能保护我们期望的方法调用的语法。它不能强制执行任何语义。前面的 MaliciousShape
类的问题在于它不尊重接口背后的意图。
LSP 指导我们避免这种错误。换句话说,LSP 指出,任何实现接口或扩展另一个类的类必须处理原始类/接口可以处理的所有输入组合。它必须提供预期的输出,不能忽略有效输入,也不能产生完全意外和不希望的行为。像这样编写的类通过对其接口的引用使用是安全的。我们的 MaliciousShape
类的问题在于它与 LSP 不兼容——它添加了一些完全意外和不希望的行为。
LSP 正式定义
美国计算机科学家 Barbara Liskov 提出了一个正式定义:如果 |
回顾形状代码中的 LSP 使用
实现 Shape
接口的所有类都符合 LSP。这在 TextBox
类中很明显,正如我们在这里看到的:
public class TextBox implements Shape {
private final String text;
public TextBox(String text) {
this.text = text;
}
@Override
public void draw(Graphics g) {
g.drawText(text);
}
}
前面的代码显然可以处理绘制提供给其构造函数的任何有效文本。它也没有提供任何意外。它使用 Graphics
类中的原语绘制文本,并且不做其他事情。
其他符合 LSP 的类的例子可以在以下类中看到:
-
Rectangle
-
Triangle
LSP
如果一个代码块可以处理所有输入并提供(至少)所有预期输出,并且没有不希望有的副作用,那么它可以安全地替换另一个代码块。 |
有一些令人惊讶的违反 LSP 的情况。也许形状代码示例中的经典例子是关于添加一个 Square
类。在数学中,正方形是一种矩形,具有高度和宽度相等的额外约束。在 Java 代码中,我们应该让 Square
类扩展 Rectangle
类吗?或者让 Rectangle
类扩展 Square
类呢?
让我们应用 LSP 来决定。我们将想象一些代码期望一个 Rectangle
类,以便它可以改变其高度,但不能改变其宽度。如果我们传递一个 Square
类给该代码,它会正常工作吗?答案是否定的。你将会有一个宽度和高度不相等的正方形。这违反了 LSP。
LSP 的重点是使类正确符合接口。在下一节中,我们将看看与 DI 密切相关的 OCP。