我听说利斯科夫替换原则(LSP)是面向对象设计的基本原则。它是什么?它的一些使用例子是什么?


当前回答

以下是这篇文章的摘录,很好地澄清了事情:

(. .为了理解一些原则,重要的是要意识到它什么时候被违反了。这就是我现在要做的。

违反这一原则意味着什么?它意味着对象不履行用接口表示的抽象所施加的契约。换句话说,这意味着您错误地识别了抽象。

考虑下面的例子:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

是否违反LSP?是的。这是因为帐户合同告诉我们帐户将被提取,但情况并非总是如此。那么,我该怎么做才能解决这个问题呢?我只是修改了合同:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

Voilà,现在合同已得到满足。

这种微妙的违反通常会使客户有能力区分所使用的具体对象之间的差异。例如,给定第一个Account的契约,它看起来像下面这样:

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

而且,这自动违反了开闭原则(即取款要求)。因为你永远不知道如果违反合同的对象没有足够的钱会发生什么。它可能什么都不返回,可能会抛出异常。所以你必须检查它是否hasEnoughMoney()——这不是接口的一部分。因此这种强制的依赖于具体类的检查违反了OCP。

这一点也解决了我经常遇到的关于LSP违反的误解。它说:“如果父母的行为在孩子身上改变了,那么它就违反了LSP。”然而,事实并非如此——只要孩子不违反父母的契约。

其他回答

这里有一个清单来确定你是否违反了利斯科夫法则。

如果你违反了以下项目之一->,你违反了里斯科夫。 如果你不违反任何->不能得出任何结论。

检查表:

No new exceptions should be thrown in derived class: If your base class threw ArgumentNullException then your sub classes were only allowed to throw exceptions of type ArgumentNullException or any exceptions derived from ArgumentNullException. Throwing IndexOutOfRangeException is a violation of Liskov. Pre-conditions cannot be strengthened: Assume your base class works with a member int. Now your sub-type requires that int to be positive. This is strengthened pre-conditions, and now any code that worked perfectly fine before with negative ints is broken. Post-conditions cannot be weakened: Assume your base class required all connections to the database should be closed before the method returned. In your sub-class you overrode that method and left the connection open for further reuse. You have weakened the post-conditions of that method. Invariants must be preserved: The most difficult and painful constraint to fulfill. Invariants are sometimes hidden in the base class and the only way to reveal them is to read the code of the base class. Basically you have to be sure when you override a method anything unchangeable must remain unchanged after your overridden method is executed. The best thing I can think of is to enforce these invariant constraints in the base class but that would not be easy. History Constraint: When overriding a method you are not allowed to modify an unmodifiable property in the base class. Take a look at these code and you can see Name is defined to be unmodifiable (private set) but SubType introduces new method that allows modifying it (through reflection): public class SuperType { public string Name { get; private set; } public SuperType(string name, int age) { Name = name; Age = age; } } public class SubType : SuperType { public void ChangeName(string newName) { var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName); } }

还有2项:方法参数的逆变性和返回类型的协方差。但这在c#中是不可能的(我是c#开发人员),所以我不关心它们。

我建议您阅读这篇文章:违反利斯科夫替换原则(LSP)。

你可以在那里找到一个解释,什么是利斯科夫替换原则,一般线索帮助你猜测你是否已经违反了它,一个方法的例子,将帮助你使你的类层次结构更安全。

利斯科夫替换原则(来自Mark Seemann的书)指出,我们应该能够在不破坏客户端或实现的情况下,用另一个接口的实现替换一个接口的实现。正是这一原则使我们能够解决未来出现的需求,即使我们今天不能预见它们。

If we unplug the computer from the wall (Implementation), neither the wall outlet (Interface) nor the computer (Client) breaks down (in fact, if it’s a laptop computer, it can even run on its batteries for a period of time). With software, however, a client often expects a service to be available. If the service was removed, we get a NullReferenceException. To deal with this type of situation, we can create an implementation of an interface that does “nothing.” This is a design pattern known as Null Object,[4] and it corresponds roughly to unplugging the computer from the wall. Because we’re using loose coupling, we can replace a real implementation with something that does nothing without causing trouble.

当一些代码认为它正在调用类型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.

在一个非常简单的句子中,我们可以说:

子类不能违背它的基类特征。它必须有能力。我们可以说这和子类型是一样的。