我听说利斯科夫替换原则(LSP)是面向对象设计的基本原则。它是什么?它的一些使用例子是什么?
当前回答
我想每个人都了解LSP在技术上是什么:你基本上希望能够从子类型细节中抽象出来,并安全地使用超类型。
所以利斯科夫有3条基本规则:
Signature Rule : There should be a valid implementation of every operation of the supertype in the subtype syntactically. Something a compiler will be able to check for you. There is a little rule about throwing fewer exceptions and being at least as accessible as the supertype methods. Methods Rule: The implementation of those operations is semantically sound. Weaker Preconditions : The subtype functions should take at least what the supertype took as input, if not more. Stronger Postconditions: They should produce a subset of the output the supertype methods produced. Properties Rule : This goes beyond individual function calls. Invariants : Things that are always true must remain true. Eg. a Set's size is never negative. Evolutionary Properties : Usually something to do with immutability or the kind of states the object can be in. Or maybe the object only grows and never shrinks so the subtype methods shouldn't make it.
所有这些属性都需要保留,并且额外的子类型功能不应该违反超类型属性。
如果这三件事都处理好了,那么您就从底层的东西中抽象出来了,并且您正在编写松散耦合的代码。
来源:程序开发在Java -芭芭拉利斯科夫
其他回答
在一个非常简单的句子中,我们可以说:
子类不能违背它的基类特征。它必须有能力。我们可以说这和子类型是一样的。
当一些代码认为它正在调用类型T的方法时,LSP是必要的,并且可能在不知情的情况下调用类型S的方法,其中S扩展了T(即S继承、派生于超类型T,或者是超类型T的子类型)。
例如,当一个函数的输入形参类型为T时,调用(即调用)的实参值类型为S。或者,当一个类型为T的标识符被赋值类型为S时,就会发生这种情况。
val id : T = new S() // id thinks it's a T, but is a S
LSP要求T类型方法(例如Rectangle)的期望(即不变量),当调用S类型方法(例如Square)时不违反此期望。
val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation
即使是具有不可变字段的类型仍然有不变量,例如,不可变的矩形设置器期望维度被独立修改,但不可变的正方形设置器违背了这一期望。
class Rectangle( val width : Int, val height : Int )
{
def setWidth( w : Int ) = new Rectangle(w, height)
def setHeight( h : Int ) = new Rectangle(width, h)
}
class Square( val side : Int ) extends Rectangle(side, side)
{
override def setWidth( s : Int ) = new Square(s)
override def setHeight( s : Int ) = new Square(s)
}
LSP要求子类型S的每个方法必须有逆变的输入参数和协变的输出。
逆变是指方差与继承方向相反,即子类型S的每个方法的每个输入参数的Si类型必须与超类型T的相应方法的相应输入参数的Ti类型相同或为超类型。
协方差是指子类型S的每个方法的输出的方差在继承的同一方向,即类型So,必须是超类型T的相应方法的相应输出的相同或类型To的子类型。
这是因为如果调用者认为它有一个类型T,认为它正在调用一个类型T的方法,那么它就会提供类型Ti的参数,并将输出分配给类型to。当它实际调用S的对应方法时,每个Ti输入参数被赋值给Si输入参数,So输出被赋值给类型to。因此,如果Si与Ti的w.r.t.不是逆变的,那么就可以将Si的子类型xi赋给Ti,而它不是Si的子类型。
此外,对于在类型多态性参数(即泛型)上具有定义-站点方差注释的语言(例如Scala或Ceylon),类型T的每个类型参数的方差注释的共方向或反方向必须分别与具有类型参数类型的每个输入参数或输出(T的每个方法)的方向相反或相同。
此外,对于每个具有函数类型的输入参数或输出,所需的方差方向是相反的。该规则是递归应用的。
子类型适用于可以枚举不变量的地方。
关于如何对不变量建模,以便由编译器强制执行,有很多正在进行的研究。
Typestate (see page 3) declares and enforces state invariants orthogonal to type. Alternatively, invariants can be enforced by converting assertions to types. For example, to assert that a file is open before closing it, then File.open() could return an OpenFile type, which contains a close() method that is not available in File. A tic-tac-toe API can be another example of employing typing to enforce invariants at compile-time. The type system may even be Turing-complete, e.g. Scala. Dependently-typed languages and theorem provers formalize the models of higher-order typing.
Because of the need for semantics to abstract over extension, I expect that employing typing to model invariants, i.e. unified higher-order denotational semantics, is superior to the Typestate. ‘Extension’ means the unbounded, permuted composition of uncoordinated, modular development. Because it seems to me to be the antithesis of unification and thus degrees-of-freedom, to have two mutually-dependent models (e.g. types and Typestate) for expressing the shared semantics, which can't be unified with each other for extensible composition. For example, Expression Problem-like extension was unified in the subtyping, function overloading, and parametric typing domains.
我的理论立场是,对于知识的存在(见章节“集中化是盲目的和不合适的”),永远不会有一个通用模型可以在图灵完备的计算机语言中强制100%覆盖所有可能的不变量。要让知识存在,就必须存在许多意想不到的可能性,即无序和熵必须总是在增加。这是熵力。证明一个潜在扩展的所有可能的计算,就是计算一个先验的所有可能的扩展。
This is why the Halting Theorem exists, i.e. it is undecidable whether every possible program in a Turing-complete programming language terminates. It can be proven that some specific program terminates (one which all possibilities have been defined and computed). But it is impossible to prove that all possible extension of that program terminates, unless the possibilities for extension of that program is not Turing complete (e.g. via dependent-typing). Since the fundamental requirement for Turing-completeness is unbounded recursion, it is intuitive to understand how Gödel's incompleteness theorems and Russell's paradox apply to extension.
对这些定理的解释将它们纳入对熵力的广义概念理解中:
Gödel's incompleteness theorems: any formal theory, in which all arithmetic truths can be proved, is inconsistent. Russell's paradox: every membership rule for a set that can contain a set, either enumerates the specific type of each member or contains itself. Thus sets either cannot be extended or they are unbounded recursion. For example, the set of everything that is not a teapot, includes itself, which includes itself, which includes itself, etc…. Thus a rule is inconsistent if it (may contain a set and) does not enumerate the specific types (i.e. allows all unspecified types) and does not allow unbounded extension. This is the set of sets that are not members of themselves. This inability to be both consistent and completely enumerated over all possible extension, is Gödel's incompleteness theorems. Liskov Substition Principle: generally it is an undecidable problem whether any set is the subset of another, i.e. inheritance is generally undecidable. Linsky Referencing: it is undecidable what the computation of something is, when it is described or perceived, i.e. perception (reality) has no absolute point of reference. Coase's theorem: there is no external reference point, thus any barrier to unbounded external possibilities will fail. Second law of thermodynamics: the entire universe (a closed system, i.e. everything) trends to maximum disorder, i.e. maximum independent possibilities.
一些补充:我想知道为什么没有人写基类的不变量、前提条件和后置条件,这些派生类必须遵守。 对于派生类D来说,基类B完全可转换,类D必须服从某些条件:
基类的内变体必须由派生类保留 派生类不能加强基类的先决条件 派生类不能削弱基类的后置条件。
因此派生类必须知道基类施加的上述三个条件。因此,子类型的规则是预先确定的。这意味着,只有当子类型遵守某些规则时,才应该遵守'IS A'关系。这些规则,以不变量、前置条件和后置条件的形式,应该由正式的“设计契约”来决定。
关于这个问题的进一步讨论可以在我的博客:利斯科夫替换原理
我在每个答案中都看到了矩形和正方形,以及如何违反LSP。
我想用一个真实的例子来展示LSP是如何符合的:
<?php
interface Database
{
public function selectQuery(string $sql): array;
}
class SQLiteDatabase implements Database
{
public function selectQuery(string $sql): array
{
// sqlite specific code
return $result;
}
}
class MySQLDatabase implements Database
{
public function selectQuery(string $sql): array
{
// mysql specific code
return $result;
}
}
这种设计符合LSP,因为无论我们选择使用哪种实现,行为都不会改变。
是的,你可以在这个配置中违反LSP,做一个简单的改变,像这样:
<?php
interface Database
{
public function selectQuery(string $sql): array;
}
class SQLiteDatabase implements Database
{
public function selectQuery(string $sql): array
{
// sqlite specific code
return $result;
}
}
class MySQLDatabase implements Database
{
public function selectQuery(string $sql): array
{
// mysql specific code
return ['result' => $result]; // This violates LSP !
}
}
现在子类型不能以同样的方式使用,因为它们不再产生相同的结果。
让我们用Java来说明:
class TrasportationDevice
{
String name;
String getName() { ... }
void setName(String n) { ... }
double speed;
double getSpeed() { ... }
void setSpeed(double d) { ... }
Engine engine;
Engine getEngine() { ... }
void setEngine(Engine e) { ... }
void startEngine() { ... }
}
class Car extends TransportationDevice
{
@Override
void startEngine() { ... }
}
这里没有问题,对吧?汽车绝对是一种交通工具,在这里我们可以看到它重写了其超类的startEngine()方法。
让我们添加另一个交通工具:
class Bicycle extends TransportationDevice
{
@Override
void startEngine() /*problem!*/
}
现在一切都不按计划进行了!是的,自行车是一种交通工具,但是,它没有发动机,因此,startEngine()方法不能实现。
这些都是违反利斯科夫代换法的问题 原则导致,他们通常可以被一个公认的 方法,该方法什么也不做,甚至不能实现。
这些问题的解决方案是一个正确的继承层次结构,在我们的例子中,我们将通过区分带引擎和不带引擎的运输设备类别来解决问题。尽管自行车是一种交通工具,但它没有发动机。在这个例子中,我们对交通工具的定义是错误的。它不应该有引擎。
我们可以像下面这样重构TransportationDevice类:
class TrasportationDevice
{
String name;
String getName() { ... }
void setName(String n) { ... }
double speed;
double getSpeed() { ... }
void setSpeed(double d) { ... }
}
现在我们可以为非机动设备扩展TransportationDevice。
class DevicesWithoutEngines extends TransportationDevice
{
void startMoving() { ... }
}
并为机动设备扩展TransportationDevice。这里更适合添加Engine对象。
class DevicesWithEngines extends TransportationDevice
{
Engine engine;
Engine getEngine() { ... }
void setEngine(Engine e) { ... }
void startEngine() { ... }
}
因此,我们的Car类变得更加专门化,同时坚持利斯科夫替换原则。
class Car extends DevicesWithEngines
{
@Override
void startEngine() { ... }
}
我们的Bicycle类也遵循利斯科夫替换原理。
class Bicycle extends DevicesWithoutEngines
{
@Override
void startMoving() { ... }
}