里氏替换原理的例子是什么?
我听说里氏替换原则(LSP)是面向对象设计的基本原则。 它是什么?它的使用示例有哪些?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
我听说里氏替换原则(LSP)是面向对象设计的基本原则。 它是什么?它的使用示例有哪些?
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
接受
或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
发布评论
评论(30)
该原则由 Barbara Liskov 于 1987 年提出,并通过关注超类及其子类型的行为来扩展开闭原则。
当我们考虑违反它的后果时,它的重要性就变得显而易见。 考虑一个使用以下类的应用程序。
想象一下,有一天,除了矩形之外,客户还要求能够操作正方形。 由于正方形是矩形,因此正方形类应该从 Rectangle 类派生。
然而,这样做我们会遇到两个问题:
正方形不需要从矩形继承高度和宽度变量,如果我们必须创建数十万个正方形对象,这可能会造成内存的显着浪费。
从矩形继承的宽度和高度设置器属性不适用于正方形,因为正方形的宽度和高度是相同的。
为了将高度和宽度设置为相同的值,我们可以创建两个新属性,如下所示:
现在,当有人设置方形对象的宽度时,其高度将相应改变,反之亦然。
让我们继续考虑另一个函数:
如果我们将对方形对象的引用传递给此函数,我们将违反 LSP,因为该函数对其参数的导数不起作用。 属性 width 和 height 不是多态的,因为它们没有在矩形中声明为虚拟(方形对象将被损坏,因为高度不会改变)。
然而,通过将 setter 属性声明为虚拟,我们将面临另一个违规行为,即 OCP。 事实上,派生类正方形的创建正在导致基类矩形发生变化。
This principle was introduced by Barbara Liskov in 1987 and extends the Open-Closed Principle by focusing on the behavior of a superclass and its subtypes.
Its importance becomes obvious when we consider the consequences of violating it. Consider an application that uses the following class.
Imagine that one day, the client demands the ability to manipulate squares in addition to rectangles. Since a square is a rectangle, the square class should be derived from the Rectangle class.
However, by doing that we will encounter two problems:
A square does not need both height and width variables inherited from the rectangle and this could create a significant waste in memory if we have to create hundreds of thousands of square objects.
The width and height setter properties inherited from the rectangle are inappropriate for a square since the width and height of a square are identical.
In order to set both height and width to the same value, we can create two new properties as follows:
Now, when someone will set the width of a square object, its height will change accordingly and vice-versa.
Let's move forward and consider this other function:
If we pass a reference to a square object into this function, we would violate the LSP because the function does not work for derivatives of its arguments. The properties width and height aren't polymorphic because they aren't declared virtual in rectangle (the square object will be corrupted because the height won't be changed).
However, by declaring the setter properties to be virtual we will face another violation, the OCP. In fact, the creation of a derived class square is causing changes to the base class rectangle.
说明 LSP 的一个很好的例子(由鲍勃叔叔在我最近听到的播客中给出)是有时在自然语言中听起来正确的东西在代码中却不太有效。
在数学中,
正方形
是矩形
。 事实上,它是矩形的特化。 “is a”让你想用继承来建模。 但是,如果在代码中您使Square
从Rectangle
派生,那么Square
应该可以在您期望Rectangle
的任何地方使用。 。 这会导致一些奇怪的行为。想象一下,您的
Rectangle
基类上有SetWidth
和SetHeight
方法; 这看起来完全符合逻辑。 但是,如果您的Rectangle
引用指向Square
,则SetWidth
和SetHeight
没有意义,因为设置一个会改变另一个来匹配它。 在这种情况下,Square
未能通过Rectangle
的里氏替换测试,并且从Rectangle
继承Square
的抽象是不好的。你们都应该看看其他无价的用励志海报解释坚实的原则。
A great example illustrating LSP (given by Uncle Bob in a podcast I heard recently) was how sometimes something that sounds right in natural language doesn't quite work in code.
In mathematics, a
Square
is aRectangle
. Indeed it is a specialization of a rectangle. The "is a" makes you want to model this with inheritance. However if in code you madeSquare
derive fromRectangle
, then aSquare
should be usable anywhere you expect aRectangle
. This makes for some strange behavior.Imagine you had
SetWidth
andSetHeight
methods on yourRectangle
base class; this seems perfectly logical. However if yourRectangle
reference pointed to aSquare
, thenSetWidth
andSetHeight
doesn't make sense because setting one would change the other to match it. In this caseSquare
fails the Liskov Substitution Test withRectangle
and the abstraction of havingSquare
inherit fromRectangle
is a bad one.Y'all should check out the other priceless SOLID Principles Explained With Motivational Posters.
里氏替换原则(LSP,lsp)是面向对象编程中的概念指出:
LSP 的核心是接口和契约,以及如何决定何时扩展类以及使用其他策略(例如组合)来实现您的目标。
我见过的说明这一点的最有效方法是Head First OOA&D。 他们提出了一个场景,您是一个项目的开发人员,负责构建策略游戏框架。
他们提供了一个表示板的类,如下所示:
所有方法都采用X 和 Y 坐标作为参数来定位
Tiles
二维数组中的图块位置。 这将允许游戏开发者在游戏过程中管理棋盘中的单位。书中还更改了要求,称游戏框架还必须支持3D游戏板,以适应具有飞行功能的游戏。 因此,引入了扩展
Board
的ThreeDBoard
类。乍一看,这似乎是一个不错的决定。
Board
提供Height
和Width
属性,ThreeDBoard
提供 Z 轴。当您查看从
Board
继承的所有其他成员时,就会出现问题。AddUnit
、GetTile
、GetUnits
等方法都采用Board
中的 X 和 Y 参数类,但ThreeDBoard
也需要 Z 参数。因此,您必须使用 Z 参数再次实现这些方法。 Z 参数与
Board
类没有上下文,并且从Board
类继承的方法失去了意义。 尝试使用ThreeDBoard
类作为其基类Board
的代码单元将非常不走运。也许我们应该寻找另一种方法。
ThreeDBoard
应该由Board
对象组成,而不是扩展Board
。 Z 轴的每个单位有一个Board
对象。这使我们能够使用良好的面向对象原则,例如封装和重用,并且不违反 LSP。
The Liskov Substitution Principle (LSP, lsp) is a concept in Object Oriented Programming that states:
At its heart LSP is about interfaces and contracts as well as how to decide when to extend a class vs. use another strategy such as composition to achieve your goal.
The most effective way I have seen to illustrate this point was in Head First OOA&D. They present a scenario where you are a developer on a project to build a framework for strategy games.
They present a class that represents a board that looks like this:
All of the methods take X and Y coordinates as parameters to locate the tile position in the two-dimensional array of
Tiles
. This will allow a game developer to manage units in the board during the course of the game.The book goes on to change the requirements to say that the game frame work must also support 3D game boards to accommodate games that have flight. So a
ThreeDBoard
class is introduced that extendsBoard
.At first glance this seems like a good decision.
Board
provides both theHeight
andWidth
properties andThreeDBoard
provides the Z axis.Where it breaks down is when you look at all the other members inherited from
Board
. The methods forAddUnit
,GetTile
,GetUnits
and so on, all take both X and Y parameters in theBoard
class but theThreeDBoard
needs a Z parameter as well.So you must implement those methods again with a Z parameter. The Z parameter has no context to the
Board
class and the inherited methods from theBoard
class lose their meaning. A unit of code attempting to use theThreeDBoard
class as its base classBoard
would be very out of luck.Maybe we should find another approach. Instead of extending
Board
,ThreeDBoard
should be composed ofBoard
objects. OneBoard
object per unit of the Z axis.This allows us to use good object oriented principles like encapsulation and reuse and doesn’t violate LSP.
Robert Martin 有一篇出色的 论文里氏替换原则。 它讨论了可能违反该原则的微妙和不那么微妙的方式。
本文的一些相关部分(请注意,第二个示例非常简洁):
Robert Martin has an excellent paper on the Liskov Substitution Principle. It discusses subtle and not-so-subtle ways in which the principle may be violated.
Some relevant parts of the paper (note that the second example is heavily condensed):
LSP 涉及不变量。
经典示例由以下伪代码声明给出(省略实现):
现在,尽管接口匹配,但我们遇到了问题。 原因是我们违反了正方形和矩形的数学定义中的不变量。 按照 getter 和 setter 的工作方式,
矩形
应满足以下不变量:但是,此不变量(以及显式后置条件)必须被 的正确实现所违反。 code>Square,因此它不是
Rectangle
的有效替代品。LSP concerns invariants.
The classic example is given by the following pseudo-code declaration (implementations omitted):
Now we have a problem although the interface matches. The reason is that we have violated invariants stemming from the mathematical definition of squares and rectangles. The way getters and setters work, a
Rectangle
should satisfy the following invariant:However, this invariant (as well as the explicit postconditions) must be violated by a correct implementation of
Square
, therefore it is not a valid substitute ofRectangle
.让我们用 Java 做一个简单的例子:
坏例子
鸭子可以飞,因为它是鸟,但是这样呢:
Ostrich是鸟,但它不会飞,Ostrich类是Bird类的子类型,但它不应该能够使用fly方法,这意味着我们违反了LSP原则。
好例子
Let's do a simple example in Java:
Bad example
The duck can fly because it is a bird, but what about this:
Ostrich is a bird, but it can't fly, Ostrich class is a subtype of class Bird, but it shouldn't be able to use the fly method, that means we are breaking the LSP principle.
Good example
我们用Java来说明一下:
这里没有问题吧? 汽车绝对是一种运输设备,在这里我们可以看到它重写了其超类的 startEngine() 方法。
让我们添加另一个交通设备:
现在一切都没有按计划进行! 是的,自行车是一种交通工具,但是它没有引擎,因此无法实现 startEngine() 方法。
这些问题的解决方案是正确的继承层次结构,在我们的例子中,我们将通过区分带引擎和不带引擎的运输设备的类别来解决问题。 尽管自行车是一种交通工具,但它没有发动机。 在这个例子中,我们对运输设备的定义是错误的。 它不应该有发动机。
我们可以重构 TransportDevice 类,如下所示:
现在我们可以为非机动设备扩展 TransportationDevice。
并将 TransportDevice 扩展为机动设备。 这里添加Engine对象是比较合适的。
因此,我们的 Car 类变得更加专业,同时遵循里氏替换原则。
而我们的 Bicycle 类也符合里氏替换原则。
Let’s illustrate in Java:
There is no problem here, right? A car is definitely a transportation device, and here we can see that it overrides the startEngine() method of its superclass.
Let’s add another transportation device:
Everything isn’t going as planned now! Yes, a bicycle is a transportation device, however, it does not have an engine and hence, the method startEngine() cannot be implemented.
The solution to these problems is a correct inheritance hierarchy, and in our case we would solve the problem by differentiating classes of transportation devices with and without engines. Even though a bicycle is a transportation device, it doesn’t have an engine. In this example our definition of transportation device is wrong. It should not have an engine.
We can refactor our TransportationDevice class as follows:
Now we can extend TransportationDevice for non-motorized devices.
And extend TransportationDevice for motorized devices. Here is is more appropriate to add the Engine object.
Thus our Car class becomes more specialized, while adhering to the Liskov Substitution Principle.
And our Bicycle class is also in compliance with the Liskov Substitution Principle.
有一个清单可以确定您是否违反了里氏规则。
检查列表:
派生类中不应抛出新的异常:如果您的基类抛出 ArgumentNullException,则您的子类只允许抛出 ArgumentNullException 类型的异常或从 ArgumentNullException 派生的任何异常。 抛出 IndexOutOfRangeException 违反了 Liskov 原则。
先决条件无法加强:假设您的基类与成员 int 一起使用。 现在你的子类型要求 int 为正数。 这是强化的先决条件,现在任何以前使用负整数工作得很好的代码都被破坏了。
后置条件不能减弱:假设您的基类要求在方法返回之前关闭所有与数据库的连接。 在您的子类中,您覆盖了该方法并保持连接打开以供进一步重用。 您削弱了该方法的后置条件。
必须保留不变量:要实现的最困难和最痛苦的约束。 不变量有时隐藏在基类中,揭示它们的唯一方法是阅读基类的代码。 基本上,您必须确保在重写方法时,执行重写的方法后,任何不可更改的内容都必须保持不变。 我能想到的最好的办法是在基类中强制执行这些不变约束,但这并不容易。
历史约束:重写方法时,不允许修改基类中不可修改的属性。 看一下这些代码,您可以看到 Name 被定义为不可修改(私有集),但 SubType 引入了允许修改它的新方法(通过反射):
还有另外 2 项:方法参数的逆变 和返回类型的协方差。 但这在 C# 中是不可能的(我是 C# 开发人员),所以我不关心它们。
There is a checklist to determine whether or not you are violating Liskov.
Check list:
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):
There are 2 others items: Contravariance of method arguments and Covariance of return types. But it is not possible in C# (I'm a C# developer) so I don't care about them.
我在每个答案中都看到了矩形和正方形,以及如何违反 LSP。
我想通过一个实际示例来展示如何使 LSP 符合:
此设计符合 LSP,因为无论我们选择使用哪种实现,行为都保持不变。
是的,您可以在此配置中违反 LSP,执行一个简单的更改,如下所示:
现在子类型不能以相同的方式使用,因为它们不再产生相同的结果。
I see rectangles and squares in every answer, and how to violate the LSP.
I'd like to show how the LSP can be conformed to with a real-world example :
This design conforms to the LSP because the behaviour remains unchanged regardless of the implementation we choose to use.
And yes, you can violate LSP in this configuration doing one simple change like so :
Now the subtypes cannot be used the same way since they don't produce the same result anymore.
当某些代码认为它正在调用
T
类型的方法,并且可能不知不觉地调用S
类型的方法时,LSP 是必要的,其中S 扩展了 T< /code> (即
S
继承、派生自超类型T
,或者是超类型T
的子类型)。例如,当使用
S
类型的参数值调用(即调用)具有T
类型的输入参数的函数时,就会发生这种情况。 或者,将T
类型的标识符分配为S
类型的值。LSP 要求
T
类型的方法(例如Rectangle
)的期望(即不变量),当S
类型的方法(例如Square
) 被调用。即使具有不可变字段的类型仍然具有不变量,例如不可变矩形设置器期望独立修改维度,但是不可变方形设置器违反了这一点期待。
LSP 要求子类型 S 的每个方法必须具有逆变输入参数和协变输出。
逆变是指方差与继承方向相反,即子类型
S
的每个方法的每个输入参数的类型Si
必须相同或相同超类型T
的相应方法的相应输入参数的类型Ti
的超类型。协方差表示方差与继承方向相同,即子类型
S
的每个方法的输出的类型So
必须相同或者< em>subtype 类型To
是超类型T
相应方法的相应输出。这是因为如果调用者认为它具有
T
类型,认为它正在调用T
的方法,那么它会提供Ti< 类型的参数/code> 并将输出分配给类型
To
。 当实际调用S
对应的方法时,则将每个Ti
输入参数分配给一个Si
输入参数,并且因此
输出被分配给类型To
。 因此,如果Si
与Ti
不是逆变,则子类型Xi
— 它不会是Si
的子类型>——可以分配给Ti
。此外,对于在类型多态性参数(即泛型)上具有定义位点方差注释的语言(例如 Scala 或 Ceylon),类型
T 必须分别与每个输入参数或输出(
T
的每个方法)相反或相同方向) 具有类型参数的类型。此外,对于具有函数类型的每个输入参数或输出,所需的方差方向是相反的。 该规则递归应用。
子类型适用于可以枚举不变量的情况。
关于如何对不变量进行建模以便编译器强制执行它们的研究正在进行中。
Typestate(参见第 3 页)声明并强制执行与类型正交的状态不变量。 或者,可以通过将断言转换为类型来强制执行不变量。 例如,要在关闭文件之前断言文件已打开,则 File.open() 可以返回 OpenFile 类型,其中包含 File 中不可用的 close() 方法。 tic-tac-toe API 可以是另一个示例使用类型来在编译时强制执行不变量。 类型系统甚至可能是图灵完备的,例如 Scala。 依赖类型语言和定理证明器形式化了高阶类型的模型。
由于需要语义抽象优于扩展,我希望使用类型来建模不变量,即统一的高阶指称语义,优于类型状态。 “扩展”意味着不协调、模块化开发的无限制、排列组合。 因为在我看来,拥有两个相互依赖的模型(例如类型和类型状态)来表达共享语义是统一和自由度的对立面,而这两个模型不能相互统一以实现可扩展的组合。 例如,Expression Problem之类的扩展在子类型、函数重载和参数类型方面得到了统一域。
我的理论立场是,知识存在(参见“中心化是盲目且不合适的”一节),永远不会有一个通用模型可以在图灵完备的计算机语言中强制 100% 覆盖所有可能的不变量。 为了使知识存在,很多意想不到的可能性都存在,即无序和熵必须总是增加。 这就是熵力。 证明潜在扩展的所有可能计算,就是先验计算所有可能的扩展。
这就是停止定理存在的原因,即图灵完备的编程语言中的每个可能的程序是否终止都是不可判定的。 可以证明某个特定的程序终止(所有可能性都已被定义和计算)。 但不可能证明该程序的所有可能的扩展都会终止,除非该程序的扩展的可能性不是图灵完备的(例如通过依赖类型)。 由于图灵完备性的基本要求是无界递归,因此可以直观地理解哥德尔不完备定理和罗素悖论如何应用于扩展。
对这些定理的解释将它们纳入对熵力的广义概念理解中:
LSP is necessary where some code thinks it is calling the methods of a type
T
, and may unknowingly call the methods of a typeS
, whereS extends T
(i.e.S
inherits, derives from, or is a subtype of, the supertypeT
).For example, this occurs where a function with an input parameter of type
T
, is called (i.e. invoked) with an argument value of typeS
. Or, where an identifier of typeT
, is assigned a value of typeS
.LSP requires the expectations (i.e. invariants) for methods of type
T
(e.g.Rectangle
), not be violated when the methods of typeS
(e.g.Square
) are called instead.Even a type with immutable fields still has invariants, e.g. the immutable Rectangle setters expect dimensions to be independently modified, but the immutable Square setters violate this expectation.
LSP requires that each method of the subtype
S
must have contravariant input parameter(s) and a covariant output.Contravariant means the variance is contrary to the direction of the inheritance, i.e. the type
Si
, of each input parameter of each method of the subtypeS
, must be the same or a supertype of the typeTi
of the corresponding input parameter of the corresponding method of the supertypeT
.Covariance means the variance is in the same direction of the inheritance, i.e. the type
So
, of the output of each method of the subtypeS
, must be the same or a subtype of the typeTo
of the corresponding output of the corresponding method of the supertypeT
.This is because if the caller thinks it has a type
T
, thinks it is calling a method ofT
, then it supplies argument(s) of typeTi
and assigns the output to the typeTo
. When it is actually calling the corresponding method ofS
, then eachTi
input argument is assigned to aSi
input parameter, and theSo
output is assigned to the typeTo
. Thus ifSi
were not contravariant w.r.t. toTi
, then a subtypeXi
—which would not be a subtype ofSi
—could be assigned toTi
.Additionally, for languages (e.g. Scala or Ceylon) which have definition-site variance annotations on type polymorphism parameters (i.e. generics), the co- or contra- direction of the variance annotation for each type parameter of the type
T
must be opposite or same direction respectively to every input parameter or output (of every method ofT
) that has the type of the type parameter.Additionally, for each input parameter or output that has a function type, the variance direction required is reversed. This rule is applied recursively.
Subtyping is appropriate where the invariants can be enumerated.
There is much ongoing research on how to model invariants, so that they are enforced by the compiler.
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.
My theoretical position is that for knowledge to exist (see section “Centralization is blind and unfit”), there will never be a general model that can enforce 100% coverage of all possible invariants in a Turing-complete computer language. For knowledge to exist, unexpected possibilities much exist, i.e. disorder and entropy must always be increasing. This is the entropic force. To prove all possible computations of a potential extension, is to compute a priori all possible extension.
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.
An interpretation of these theorems incorporates them in a generalized conceptual understanding of the entropic force:
长故事简短,让我们留下矩形矩形和正方形正方形,扩展父类时的实际示例,您必须保留确切的父 API 或扩展它。
假设您有一个基本 ItemsRepository。
以及扩展它的子类:
然后您可以拥有一个使用 Base ItemsRepository API 并依赖它的 Client。
当用子类替换父类破坏了API的契约时,LSP就会被破坏。
您可以在我的课程中了解有关编写可维护软件的更多信息:https://www.udemy.com/enterprise- php/
Long story short, let's leave rectangles rectangles and squares squares, practical example when extending a parent class, you have to either PRESERVE the exact parent API or to EXTEND IT.
Let's say you have a base ItemsRepository.
And a sub class extending it:
Then you could have a Client working with the Base ItemsRepository API and relying on it.
The LSP is broken when substituting parent class with a sub class breaks the API's contract.
You can learn more about writing maintainable software in my course: https://www.udemy.com/enterprise-php/
我想每个人都已经了解了 LSP 在技术上的含义:您基本上希望能够从子类型细节中抽象出来并安全地使用超类型。
所以里氏有 3 个基本规则:
签名规则:在语法上,子类型中的超类型的每个操作都应该有一个有效的实现。 编译器可以为您检查一些内容。 有一条关于抛出更少异常并且至少与超类型方法一样可访问的小规则。
方法规则:这些操作的实现在语义上是合理的。
属性规则:这超出了单个函数调用的范围。
所有这些属性都需要保留,并且额外的子类型功能不应违反超类型属性。
如果考虑到这三件事,您就已经从底层的东西中抽象出来,并且正在编写松散耦合的代码。
资料来源:Java 程序开发 - Barbara Liskov
I guess everyone kind of covered what LSP is technically: You basically want to be able to abstract away from subtype details and use supertypes safely.
So Liskov has 3 underlying rules:
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.
Properties Rule : This goes beyond individual function calls.
All these properties need to be preserved and the extra subtype functionality shouldn't violate supertype properties.
If these three things are taken care of , you have abstracted away from the underlying stuff and you are writing loosely coupled code.
Source: Program Development in Java - Barbara Liskov
LSP 是关于类契约的规则:如果基类满足契约,那么 LSP 派生类也必须满足该契约。
在 Pseudo-python 中,
如果每次在 Derived 对象上调用 Foo 时,只要 arg 相同,它都会给出与在 Base 对象上调用 Foo 完全相同的结果,则满足 LSP。
The LSP is a rule about the contract of the clases: if a base class satisfies a contract, then by the LSP derived classes must also satisfy that contract.
In Pseudo-python
satisfies LSP if every time you call Foo on a Derived object, it gives exactly the same results as calling Foo on a Base object, as long as arg is the same.
里氏替换原则
The Liskov Substitution Principle
当我第一次阅读 LSP 时,我认为这是一个非常严格的含义,本质上将其等同于接口实现和类型安全转换。 这意味着 LSP 要么由语言本身保证,要么不由语言本身保证。 例如,从严格意义上讲,就编译器而言,ThreeDBoard 肯定可以替代 Board。
在阅读了更多关于这个概念的内容后,我发现 LSP 通常的解释比这更广泛。
简而言之,客户端代码“知道”指针后面的对象是派生类型而不是指针类型意味着什么,并不限于类型安全。 还可以通过探测对象的实际行为来测试对 LSP 的遵守情况。 也就是说,检查对象的状态和方法参数对方法调用结果的影响,或者对象抛出的异常类型。
再次回到这个例子,理论上 Board 方法可以在 ThreeDBoard 上正常工作。 然而在实践中,在不妨碍 ThreeDBoard 打算添加的功能的情况下,很难防止客户端可能无法正确处理的行为差异。
有了这些知识,评估 LSP 遵守情况可以成为一个很好的工具,可以确定组合何时是扩展现有功能(而不是继承)更合适的机制。
When I first read about LSP, I assumed that this was meant in a very strict sense, essentially equating it to interface implementation and type-safe casting. Which would mean that LSP is either ensured or not by the language itself. For example, in this strict sense, ThreeDBoard is certainly substitutable for Board, as far as the compiler is concerned.
After reading up more on the concept though I found that LSP is generally interpreted more broadly than that.
In short, what it means for client code to "know" that the object behind the pointer is of a derived type rather than the pointer type is not restricted to type-safety. Adherence to LSP is also testable through probing the objects actual behavior. That is, examining the impact of an object's state and method arguments on the results of the method calls, or the types of exceptions thrown from the object.
Going back to the example again, in theory the Board methods can be made to work just fine on ThreeDBoard. In practice however, it will be very difficult to prevent differences in behavior that client may not handle properly, without hobbling the functionality that ThreeDBoard is intended to add.
With this knowledge in hand, evaluating LSP adherence can be a great tool in determining when composition is the more appropriate mechanism for extending existing functionality, rather than inheritance.
LSP使用的一个重要例子是软件测试。
如果我有一个类 A,它是 B 的符合 LSP 的子类,那么我可以重用 B 的测试套件来测试 A。
要完全测试子类 A,我可能需要添加更多测试用例,但至少我可以重用超类 B 的所有测试用例。
实现这一点的一种方法是构建 McGregor 所说的“用于测试的并行层次结构”:我的
ATest
类将从BTest
继承。 然后需要某种形式的注入来确保测试用例适用于 A 类型的对象而不是 B 类型的对象(一个简单的模板方法模式即可)。请注意,对所有子类实现重用超级测试套件实际上是测试这些子类实现是否符合 LSP 的一种方法。 因此,人们也可以认为应该在任何子类的上下文中运行超类测试套件。
另请参阅 Stackoverflow 问题“我可以实现一系列可重用测试来测试接口的实现吗?”的答案
An important example of the use of LSP is in software testing.
If I have a class A that is an LSP-compliant subclass of B, then I can reuse the test suite of B to test A.
To fully test subclass A, I probably need to add a few more test cases, but at the minimum I can reuse all of superclass B's test cases.
A way to realize is this by building what McGregor calls a "Parallel hierarchy for testing": My
ATest
class will inherit fromBTest
. Some form of injection is then needed to ensure the test case works with objects of type A rather than of type B (a simple template method pattern will do).Note that reusing the super-test suite for all subclass implementations is in fact a way to test that these subclass implementations are LSP-compliant. Thus, one can also argue that one should run the superclass test suite in the context of any subclass.
See also the answer to the Stackoverflow question "Can I implement a series of reusable tests to test an interface's implementation?"
简单来说,LSP 指出相同的对象超类应该能够在不破坏任何内容的情况下相互交换。
例如,如果我们有一个从
Animal
类派生的Cat
和Dog
类,则任何使用 Animal 类的函数都应该能够使用Cat
或Dog
并且行为正常。The LSP in simple terms states that objects of the same superclass should be able to be swapped with each other without breaking anything.
For example, if we have a
Cat
and aDog
class derived from anAnimal
class, any functions using the Animal class should be able to useCat
orDog
and behave normally.用一句很简单的话来说,我们可以说:
子类不能违背它的基类特性。 它必须有能力做到这一点。 我们可以说它与子类型相同。
In a very simple sentence, we can say:
The child class must not violate its base class characteristics. It must be capable with it. We can say it's same as subtyping.
示例:
下面是违反里氏替换原理的经典示例。 在示例中,使用了 2 个类:矩形和正方形。 假设在应用程序中的某个地方使用了 Rectangle 对象。 我们扩展该应用程序并添加 Square 类。 square 类是由工厂模式返回的,基于某些条件,我们不知道具体会返回什么类型的对象。 但我们知道它是一个矩形。 我们获取矩形对象,将宽度设置为 5,高度设置为 10,并获取面积。 对于宽度为 5、高度为 10 的矩形,面积应为 50。相反,结果将为 100
另请参阅:开闭原则
一些类似的概念,以获得更好的结构: 约定优于配置
Example:
Below is the classic example for which the Liskov's Substitution Principle is violated. In the example, 2 classes are used: Rectangle and Square. Let's assume that the Rectangle object is used somewhere in the application. We extend the application and add the Square class. The square class is returned by a factory pattern, based on some conditions and we don't know the exact what type of object will be returned. But we know it's a Rectangle. We get the rectangle object, set the width to 5 and height to 10 and get the area. For a rectangle with width 5 and height 10, the area should be 50. Instead, the result will be 100
See also: Open Close Principle
Some similar concepts for better structure: Convention over configuration
LSP 的这个表述太强了:
。基本上意味着 S 是另一个完全封装的实现,与 T 完全相同。我可以大胆地决定性能是 P 行为的一部分......
所以,基本上,任何后期绑定的使用都会违反 LSP 。 OO 的全部意义在于,当我们用一种对象替换另一种对象时,会获得不同的行为!
维基百科引用的表述更好,因为该属性取决于上下文并且不一定包括整个程序的行为。
This formulation of the LSP is way too strong:
Which basically means that S is another, completely encapsulated implementation of the exact same thing as T. And I could be bold and decide that performance is part of the behavior of P...
So, basically, any use of late-binding violates the LSP. It's the whole point of OO to to obtain a different behavior when we substitute an object of one kind for one of another kind!
The formulation cited by wikipedia is better since the property depends on the context and does not necessarily include the whole behavior of the program.
一些附录:
我想知道为什么没有人写关于派生类必须遵守的基类的 Invariant 、前置条件和后置条件。
为了使派生类 D 完全可由基类 B 替代,类 D 必须遵守某些条件:
因此,派生类必须了解基类强加的上述三个条件。 因此,子类型化的规则是预先决定的。 这意味着,只有当子类型遵守某些规则时,才应遵守“IS A”关系。 这些规则,以不变量、前置条件和后置条件的形式,应由正式的“设计契约'。
有关此问题的进一步讨论,请参见我的博客:里氏替换原理< /a>
Some addendum:
I wonder why didn't anybody write about the Invariant , preconditions and post conditions of the base class that must be obeyed by the derived classes.
For a derived class D to be completely sustitutable by the Base class B, class D must obey certain conditions:
So the derived must be aware of the above three conditions imposed by the base class. Hence, the rules of subtyping are pre-decided. Which means, 'IS A' relationship shall be obeyed only when certain rules are obeyed by the subtype. These rules, in the form of invariants, precoditions and postcondition, should be decided by a formal 'design contract'.
Further discussions on this available at my blog: Liskov Substitution principle
它指出,如果 C 是 E 的子类型,则可以用 C 类型的对象替换 E,而不会更改或破坏程序的行为。 简而言之,派生类应该可以替换其父类。 例如,如果农民的儿子是农民,那么他可以代替他的父亲工作,但如果农民的儿子是板球运动员,那么他就不能代替他的父亲工作父亲。
违规示例:
在给定的示例中,
FighterPlane
和PaperPlane
类都扩展了包含startEngine()
方法的Plane
类。 因此很明显,FighterPlane
可以启动引擎,但PaperPlane
不能,因此它破坏了LSP
。PaperPlane
类虽然扩展了Plane
类并且应该可以替换它,但它不是 Plane 实例可以替换的合格实体,因为纸飞机无法启动引擎,因为它没有引擎。 所以好的例子是,受人尊敬的例子:
It states that if C is a subtype of E then E can be replaced with objects of type C without changing or breaking the behavior of the program. In simple words, derived classes should be substitutable for their parent classes. For example, if a Farmer’s son is Farmer then he can work in place of his father but if a Farmer’s son is a cricketer then he can’t work in place of his father.
Violation Example:
In the given example
FighterPlane
andPaperPlane
classes both extending thePlane
class which containstartEngine()
method. So it's clear thatFighterPlane
can start engine butPaperPlane
can’t so it’s breakingLSP
.PaperPlane
class although extendingPlane
class and should be substitutable in place of it but is not an eligible entity that Plane’s instance could be replaced by, because a paper plane can’t start the engine as it doesn’t have one. So the good example would be,Respected Example:
大局观:
示例
与其他答案不同,我不会从违反里氏替换原则 (LSP) 开始,而是从遵守 LSP 开始。 我使用 Java,但在每种 OOP 语言中几乎都是一样的。
Circle
和ColoredCircle
几何示例在这里似乎很受欢迎。
半径不允许为负数。 这是一个子类:
根据 LSP,该子类是
Circle
的子类型。LSP 指出:
这里,对于每个
ColoredCircle
实例o1
,考虑具有相同半径o2
的Circle
实例。 对于每个使用Circle
对象的程序,如果将o2
替换为o1
,则任何使用Circle
的程序的行为都会替换后保持不变。 (请注意,这是理论上的:使用ColoredCircle
实例比使用Circle
实例更快地耗尽内存,但这与这里无关。)我们如何找到
o2
取决于o1
? 我们只删除color
属性并保留radius
属性。 我将这个转换称为o1
->o2
从CircleColor
空间到Circle
空间的投影。反例
让我们再创建一个例子来说明 LSP 的违规情况。
Circle
和Square
想象一下前面的
Circle
类的子类:违反 LSP
现在,看看这个程序:
我们用以下命令测试该程序 :一个
Circle
对象和一个Square
对象。发生了什么 ? 直观上,虽然
Square
是Circle
的子类,但Square
并不是Circle 的子类型code> 因为常规
Circle
实例的半径不会为 -1。从形式上来说,这违反了里氏替换原则。
我们有一个根据
Circle
定义的程序,并且没有Circle
对象可以替换new Square(2)
(或任何顺便说一下,此程序中的 Square
实例)并保持行为不变:记住任何Circle
的半径始终为正。子类和子类型
现在我们知道为什么子类并不总是子类型。 当子类不是子类型时,即当存在 LSP 违规时,某些程序(至少一个)的行为并不总是预期的行为。 这是非常令人沮丧的,通常被解释为一个错误。
在理想的世界中,编译器或解释器将能够检查给定的子类是否是真正的子类型,但我们并不处于理想的世界中。
静态类型
如果有一些静态类型,您将在编译时受到超类签名的约束。
Square.getRadius()
无法返回String
或List
。如果没有静态类型,如果一个参数的类型错误(除非类型很弱)或参数数量不一致(除非语言非常宽松),那么您将在运行时收到错误。
关于静态类型的注意事项:存在返回类型的协变机制(S 的方法可以返回 T 的同一方法的返回类型的子类)和参数类型的逆变机制(S 的方法)可以接受 T) 的相同方法的相同参数的超类。 这是下面解释的前置条件和后置条件的具体情况。
按契约设计
还有更多。 有些语言(我认为是 Eiffel)提供了一种强制遵守 LSP 的机制。
更不用说确定初始对象
o1
的投影o2
,如果将o1
替换为,我们可以预期任何程序都会有相同的行为>o2
if,对于任何参数x
和任何方法f
:o2.f(x) 是一个有效的调用,那么
o1.f(x)
也应该是一个有效的调用 (1)。o1.f(x)
的结果(返回值、在控制台上显示等)应等于o2.f(x)
的结果,或者至少同样有效 (2)。o1.f(x)
应该让o1
处于内部状态,而o2.f(x)
应该让o2
> 处于内部状态,以便下一个函数调用将确保 (1)、(2) 和 (3) 仍然有效 (3)。(请注意,如果函数
f
是纯函数,则 (3) 是免费给出的。这就是我们喜欢拥有不可变对象的原因。)这些条件与语义有关 类的(期望什么),而不仅仅是类的语法。 而且,这些条件都非常强大。 但它们可以通过合约编程设计中的断言来近似。 这些断言是确保维护类型语义的一种方法。 违反契约会导致运行时错误。
Sf
接受多于Tf
)(a)。Sf
比Tf
提供更多)(b)。我们粗略地看到,(a) 确保了 (1),(b) 确保了 (2),但 (c) 弱于 (3)。 此外,断言有时很难表达。
想象一个类
Counter
具有返回下一个整数的唯一方法Counter.counter()
。 你如何为此编写后置条件? 想象一个类Random
具有一个Random.gaussian()
方法,该方法返回 0.0 和 1.0 之间的浮点数。 如何编写后置条件来检查分布是否为高斯分布? 也许有可能,但成本太高,以至于我们将依赖测试而不是后置条件。结论
不幸的是,子类并不总是子类型。 这可能会导致意外行为——错误。
OOP 语言提供了避免这种情况的机制。 首先在句法层面。 在语义层面上也是如此,具体取决于编程语言:部分语义可以使用断言编码在程序的文本中。 但由您来确保子类是子类型。
还记得您什么时候开始学习 OOP 的吗? “如果关系是 IS-A,则使用继承”。 反之亦然:如果使用继承,请确保关系是 IS-A。
LSP 在比断言更高的级别上定义了什么是子类型。 断言是确保 LSP 得到支持的宝贵工具。
The big picture :
Example
Unlike the other answers, I won't start with a Liskov Substitution Principle (LSP) violation, but with a LSP compliance. I use Java but it would be almost the same in every OOP language.
Circle
andColoredCircle
Geometrical examples seem pretty popular here.
The radius is not allowed to be negative. Here's a suclass:
This subclass is a subtype of
Circle
, according to the LSP.The LSP states that:
Here, for each
ColoredCircle
instanceo1
, consider theCircle
instance having the same radiuso2
. For every program usingCircle
objects, if you replaceo2
byo1
, the behavior of any program usingCircle
will remain the same after the substitution. (Note that this is theoretical : you will exhaust the memory faster usingColoredCircle
instances than usingCircle
instances, but that's not relevant here.)How do we find the
o2
depending ono1
? We just strip thecolor
attribute and keep theradius
attribute. I call the transformationo1
->o2
a projection from theCircleColor
space on theCircle
space.Counter Example
Let's create another example to illustrate the violation of the LSP.
Circle
andSquare
Imagine this subclass of the previous
Circle
class:The violation of the LSP
Now, look at this program :
We test the program with a
Circle
object and with aSquare
object.What happened ? Intuitively, although
Square
is a subclass ofCircle
,Square
is not a subtype ofCircle
because no regularCircle
instance would ever have a radius of -1.Formally, this is a violation of Liskov Substitution Principle.
We have a program defined in terms of
Circle
and there is noCircle
object that can replacenew Square(2)
(or anySquare
instance by the way) in this program and leave the behavior unchanged: remember that radius of anyCircle
is always positive.Subclass and subtype
Now we know why a subclass is not always subtype. When a subclass is not a subtype, i.e. when there is a LSP violation, the behavior of some programs (at least one) won't always be the expected behavior. This is very frustrating and is usually interpreted as a bug.
In an ideal world, the compiler or interpreter would be able to check is a given subclass is a real subtype, but we are not in an ideal world.
Static typing
If there is some static typing, you are bound by the superclass signature at compile time.
Square.getRadius()
can't return aString
or aList
.If there is no static typing, you'll get an error at runtime if the type of one argument is wrong (unless the typing is weak) or the number of arguments is inconsistent (unless the language is very permissive).
Note about the static typing: there is a mechanism of covariance of the return type (a method of S can return a subclass of the return type of the same method of T) and contravariance of the parameters types (a method of S can accept a superclass of a parameter of the same parameter of the same method of T). That is a specific case of precondition and postcondition explained below.
Design by contract
There's more. Some languages (I think of Eiffel) provide a mechanism to enforce the compliance with the LSP.
Let alone the determination the projection
o2
of the initial objecto1
, we can expect the same behavior of any program ifo1
is substituted foro2
if, for any argumentx
and any methodf
:o2.f(x)
is a valid call, theno1.f(x)
should also be a valid call (1).o1.f(x)
should be equal to the result ofo2.f(x)
, or at least equally valid (2).o1.f(x)
should leto1
in an internal state ando2.f(x)
should leto2
in an internal state so that next function calls will ensure that (1), (2) and (3) will still be valid (3).(Note that (3) is given for free if the function
f
is pure. That's why we like to have immutable objects.)These conditions are about the semantics (what to expect) of the class, not only the syntax of the class. Also, these conditions are very strong. But they can be approximated by assertions in design by contract programming. These assertions are a way to ensure that the semantic of the type is upheld. Breaking the contract leads to runtime errors.
S.f
accepts more thanT.f
) (a).S.f
provides more thanT.f
) (b).We see that, roughly, (a) ensures (1) and (b) ensures (2), but (c) is weaker than (3). Moreover, assertions are sometimes difficult to express.
Think of a class
Counter
having a unique methodCounter.counter()
that returns the next integer. How do you write a postcondition for that ? Think of a classRandom
having a methodRandom.gaussian()
that returns a float between 0.0 and 1.0 . How do you write a postcondition to check that the distribution is gaussian ? It may be possible, but the cost would be so high that we would rely on test rather than on postconditions.Conclusion
Unfortunately, a subclass is not always a subtype. This can lead to an unexpected behavior -- a bug.
OOP languages provide mechanism to avoid this situation. At syntactic level first. At semantical level too, depending on the programming language: a part of the semantics can be encoded in the text of the program using assertions. But it's up to you to ensure that a subclass is a subtype.
Remember when you began to learn OOP ? "If the relation is IS-A, then use inheritance". That's true the other way: if you use inheritance, be sure that the relation is IS-A.
The LSP defines, at a higher level than assertions, what is a subtype. Assertions are a valuable tool to ensure that the LSP is upheld.
假设我们在代码中使用矩形。
在几何类中,我们了解到正方形是一种特殊类型的矩形,因为它的宽度与高度相同。 让我们根据以下信息创建一个
Square
类:如果我们在第一个代码中将
Rectangle
替换为Square
,那么它将崩溃:这是因为
Square
有一个新的前提条件,而我们在Rectangle
类中没有:width == height
。 根据 LSP,Rectangle
实例应该可以用Rectangle
子类实例替换。 这是因为这些实例通过了Rectangle
实例的类型检查,因此它们将在您的代码中导致意外错误。这是 维基文章。 总而言之,违反 LSP 可能会在某些时候导致代码错误。
Let's say we use a rectangle in our code
In our geometry class we learned that a square is a special type of rectangle because its width is the same length as its height. Let's make a
Square
class as well based on this info:If we replace the
Rectangle
withSquare
in our first code, then it will break:This is because the
Square
has a new precondition we did not have in theRectangle
class:width == height
. According to LSP theRectangle
instances should be substitutable withRectangle
subclass instances. This is because these instances pass the type check forRectangle
instances and so they will cause unexpected errors in your code.This was an example for the "preconditions cannot be strengthened in a subtype" part in the wiki article. So to sum up, violating LSP will probably cause errors in your code at some point.
用 Board 数组来实现 ThreeDBoard 有用吗?
也许您可能希望将不同平面中的 ThreeDBoard 切片视为 Board。 在这种情况下,您可能需要为 Board 抽象出一个接口(或抽象类)以允许多种实现。
在外部接口方面,您可能希望为 TwoDBoard 和 ThreeDBoard 提取一个 Board 接口(尽管上述方法都不适合)。
Would implementing ThreeDBoard in terms of an array of Board be that useful?
Perhaps you may want to treat slices of ThreeDBoard in various planes as a Board. In that case you may want to abstract out an interface (or abstract class) for Board to allow for multiple implementations.
In terms of external interface, you might want to factor out a Board interface for both TwoDBoard and ThreeDBoard (although none of the above methods fit).
到目前为止,我发现的对LSP最清晰的解释是“里氏替换原则说派生类的对象应该能够替换基类的对象,而不会给系统带来任何错误或修改基类的行为”来自此处。 本文提供了违反 LSP 并修复它的代码示例。
The clearest explanation for LSP I found so far has been "The Liskov Substitution Principle says that the object of a derived class should be able to replace an object of the base class without bringing any errors in the system or modifying the behavior of the base class" from here. The article gives code example for violating LSP and fixing it.
正方形是宽度等于高度的矩形。 如果正方形为宽度和高度设置两个不同的大小,则会违反正方形不变量。 这是通过引入副作用来解决的。 但如果矩形有一个 setSize(height, width) 且前提条件 0 < 高度和 0 < 宽度。 派生子类型方法要求height == width; 更强的前提条件(这违反了 lsp)。 这表明,尽管 square 是矩形,但它不是有效的子类型,因为前提条件得到了加强。 解决办法(通常是一件坏事)会产生副作用,这会削弱后置条件(违反了 lsp)。 底座上的 setWidth 具有后置条件 0 < 宽度。 导出的高度==宽度削弱了它。
因此,可调整大小的正方形不是可调整大小的矩形。
A square is a rectangle where the width equals the height. If the square sets two different sizes for the width and height it violates the square invariant. This is worked around by introducing side effects. But if the rectangle had a setSize(height, width) with precondition 0 < height and 0 < width. The derived subtype method requires height == width; a stronger precondition (and that violates lsp). This shows that though square is a rectangle it is not a valid subtype because the precondition is strengthened. The work around (in general a bad thing) cause a side effect and this weakens the post condition (which violates lsp). setWidth on the base has post condition 0 < width. The derived weakens it with height == width.
Therefore a resizable square is not a resizable rectangle.
里氏替换原则
[SOLID]
继承子类型化
Wiki Liskov 替换原则 (LSP)
*Precondition + Postcondition =
函数(方法)类型
[Swift 函数类型。 Swift 函数与方法]示例
前提条件(例如函数
参数类型
)可以相同或较弱(争取 -> C1)后置条件(例如函数
返回类型) 可以相同或更强(争取 -> C3)
不变变量超级类型的[关于]应保持不变
Swift
Java 中
保持不变行为子类型< /strong>
Wiki Liskov 替换原则 (LSP)
[方差、协方差、逆变、不变]
Liskov Substitution Principle
[SOLID]
Inheritance Subtyping
Wiki Liskov substitution principle (LSP)
*Precondition + Postcondition =
function (method) types
[Swift Function type. Swift function vs method]Example
Preconditions(e.g. function
parameter type
) can be the same or weaker(strives for -> C1)Postconditions(e.g. function
returned type
) can be the same or stronger(strives for -> C3)Invariant variable[About] of super type should stay invariant
Swift
Java
Behavioral subtyping
Wiki Liskov substitution principle (LSP)
[Variance, Covariance, Contravariance, Invariance]
LSP 说“对象应该可以被它们的子类型替换”。
另一方面,这个原则指出
下面的例子有助于更好地理解LSP。
没有LSP:
通过LSP修复:
LSP says that ''Objects should be replaceable by their subtypes''.
On the other hand, this principle points to
and the following example helps to have a better understanding of LSP.
Without LSP:
Fixing by LSP:
我鼓励您阅读这篇文章:违反里氏替换原则 (LSP) 。
您可以在那里找到什么是里氏替换原则的解释、帮助您猜测是否已经违反了该原则的一般线索以及帮助您使类层次结构更加安全的方法示例。
I encourage you to read the article: Violating Liskov Substitution Principle (LSP).
You can find there an explanation what is the Liskov Substitution Principle, general clues helping you to guess if you have already violated it and an example of approach that will help you to make your class hierarchy be more safe.