什么是Liskov替代原则的例子?
我听说Liskov替代原则(LSP)是面向对象devise的基本原则。 这是什么,它有什么使用的例子?
说明LSP的一个很好的例子(鲍勃叔叔在我最近听到的一个播客中给出的)是有时候自然语言中听起来正确的东西在代码中是不太适用的。
在math中, Square
是Rectangle
。 事实上,这是一个矩形的专业化。 “是”使你想用inheritance来build模。 但是,如果你在Square
中的代码是从Rectangle
派生的,那么Square
应该在你期待的Rectangle
任何地方都可以使用。 这使得一些奇怪的行为。
想象一下你的Rectangle
基类有SetWidth
和SetHeight
方法; 这似乎完全合乎逻辑。 但是,如果您的Rectangle
引用指向Square
,那么SetWidth
和SetHeight
没有意义,因为设置一个会更改另一个以匹配它。 在这种情况下, Square
不能通过Rectangle
进行Liskovreplacetesting,而从Square
Rectangle
inheritance的抽象是一个糟糕的问题。
你们应该看看其他无价的固体原则激励海报 。
Liskovreplace原则(LSP, lsp )是面向对象编程中的一个概念,它指出:
使用指针或基类的引用的函数必须能够使用派生类的对象而不知道它。
它的核心是关于接口和契约,以及如何决定何时扩展一个类,如何使用另一个策略,如组合来实现你的目标。
我所看到的最有效的方法是在头一个OOA&D 。 他们提出了一个场景,在这个场景中,你是一个开发项目的开发者,构build一个战略游戏框架
他们提出了一个代表董事会的类,如下所示:
所有这些方法都以X和Y坐标为参数来定位Tiles
的二维数组中的图块位置。 这将允许游戏开发者在游戏过程中pipe理棋盘上的单位。
本书继续改变要求说,游戏框架的工作还必须支持3D游戏板,以适应有飞行的游戏。 所以引入了扩展Board
的ThreeDBoard
类。
乍一看这似乎是一个很好的决定。 Board
提供Height
和Width
属性, ThreeDBoard
提供Z轴。
当你看到从Board
inheritance的所有其他成员。 AddUnit
, GetTile
, GetUnits
等方法都是在Board
类中同时使用X和Y参数,而ThreeDBoard
需要Z参数。
所以你必须用Z参数再次实现这些方法。 Z参数对于Board
类没有上下文,从Board
类inheritance的方法失去了意义。 试图使用ThreeDBoard
类作为其基类Board
的代码单元将是非常不幸的。
也许我们应该find另一种方法。 ThreeDBoard
不应该扩展Board
, ThreeDBoard
应该由Board
对象组成。 一个Z轴的单位对象。
这使我们能够使用像封装和重用这样的好的面向对象的原则,而不会违反LSP。
LSP关注不variables。 你的开发板的例子在一开始就被打破了,因为接口不匹配。
一个更好的例子是以下(实现略):
class Rectangle { int getHeight() const; void setHeight(int value); int getWidth() const; void setWidth(int value); }; class Square : public Rectangle { };
现在我们遇到了一个问题,虽然界面匹配。 原因是我们违反了由正方形和矩形的math定义引起的不variables。 getter和setter的工作方式, Rectangle
应该满足以下不变:
void invariant(Rectangle& r) { r.setHeight(200); r.setWidth(100); assert(r.getHeight() == 200 and r.getWidth() == 100); }
然而,这个不变式必须被Square
的正确实现所侵害,因此它不是Rectangle
的有效替代。
罗伯特·马丁有关里斯科换人原则的优秀论文 。 它讨论了可能违反原则的微妙和不那么微妙的方式。
本文的一些相关部分(请注意,第二个例子严重凝结):
一个违反LSP的简单例子
其中最明显的违反这个原则的是使用C ++运行时types信息(RTTI)来根据对象的typesselect函数。 即:
void DrawShape(const Shape& s) { if (typeid(s) == typeid(Square)) DrawSquare(static_cast<Square&>(s)); else if (typeid(s) == typeid(Circle)) DrawCircle(static_cast<Circle&>(s)); }
很显然,
DrawShape
函数形成的很糟糕。 它必须知道Shape
类的每个可能派生物,并且每当创buildShape
新派生物时都必须更改它。 事实上,许多人把这个function的结构视为对面向对象devise的诅咒。广场和矩形,更微妙的违规。
但是,还有其他更微妙的方法来违反LSP。 考虑一个使用
Rectangle
类的应用程序,如下所述:class Rectangle { public: void SetWidth(double w) {itsWidth=w;} void SetHeight(double h) {itsHeight=w;} double GetHeight() const {return itsHeight;} double GetWidth() const {return itsWidth;} private: double itsWidth; double itsHeight; };
想象一下,有一天,用户需要能够操纵正方形以外的正方形。 […]
显然,正方形对于所有正常的意图和目的是矩形的。 由于ISA关系成立,将
Square
类build模为从Rectangle
派生是合乎逻辑的。 […]
Square
将inheritanceSetWidth
和SetHeight
函数。 这些函数完全不适合Square
,因为Square
的宽度和高度是相同的。 这应该是devise中存在问题的重要线索。 但是,有一种方法可以避开这个问题。 我们可以重写SetWidth
和SetHeight
[…]但是请考虑以下function:
void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth }
如果我们将一个
Square
对象的引用传递给这个函数,Square
对象将被破坏,因为高度不会被改变。 这明显违反了LSP。 该函数不适用于其参数的派生。[…]
当一些代码认为它正在调用typesT
的方法时,LSP是必要的,并且可能在不知不觉中调用typesS
的方法,其中S extends T
(即S
从超typesT
inheritance,派生或者是子types) 。
例如,这发生在具有typesT
的input参数的函数被调用(即被调用)的types为S
的参数值的情况下。 或者,在typesT
的标识符被分配了typesS
的值。
val id : T = new S() // id thinks it's a T, but is a S
LSP需要typesT
(例如Rectangle
)方法的期望值(即不variables),而不是在调用typesS
(例如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
即使是具有不可变字段的types仍然具有不变式,例如不可变的 Rectangle设置器期望维度被独立修改,但不可变的 Square设置器违反了这个期望。
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要求子typesS
每个方法必须具有逆变input参数和协变输出。
相反意味着方差与inheritance方向相反,即子typesS
的每个方法的每个input参数的typesSi
必须是相应方法的相应input参数的typesTi
的相同types或超types的超typesT
协方差意味着方差与inheritance方向相同,也就是说,子typesS
的每个方法的输出的typesS
必须是相同或相应方法输出的types的子typesTo
超typesT
这是因为如果调用者认为它有一个typesT
,认为它正在调用T
一个方法,那么它提供Ti
types的参数,并将输出分配给typesTo
。 当它实际调用S
的相应方法时,则将每个Ti
input参数分配给一个Si
input参数,并将So
输出分配给typesTo
。 因此,如果Si
不与Ti
逆转,那么将不属于Si
亚型的亚型Xi可分配给Ti
。
此外,对于在types多态性参数(即generics)上具有定义位置变异注释的语言(例如Scala或Ceylon),typesT
每个types参数的方差注释的同向或反向必须相反或相同方向分别指向具有types参数types的每个input参数或每个T
方法的输出。
另外,对于每个具有函数types的input参数或输出,所需的方差方向都是相反的。 这个规则是recursion地应用的。
子types适用于可以枚举不variables的地方。
目前对于如何对不variables进行build模的研究还有很多,因此编译器会强制执行。
Typestate (参见第3页)声明并强制与types正交的状态不variables。 或者,不变式可以通过将断言转换为types来强制执行。 例如,要断言文件在closures之前是打开的,那么File.open()就可以返回一个OpenFiletypes,它包含一个在File中不可用的close()方法。 井字游戏API可以是使用打字在编译时强制执行不变的另一个示例。 types系统甚至可能是图灵完整的,例如Scala 。 依赖型语言和定理certificate者将高阶input模型forms化。
由于需要通过扩展来抽象语义,所以我期望用types来build模不variables,即统一的高阶指示语义,要优于Typestate。 “延伸”是指不协调的,模块化的发展的无限,排列组合。 因为在我看来,它是统一的对立面,因而也是自由度,有两个相互依赖的模型(例如types和types状态)用于expression共享语义,而这些模式不能相互统一以获得可扩展的组合。 例如, expression式类问题的扩展在子types,函数重载和参数化types域中是统一的。
我的理论立场是知识存在 (见“集中化是盲目的,不合适的”), 永远不会有一个通用的模型,可以强化图灵完全计算机语言中所有可能的不variables的100%覆盖。 为了知识的存在,许多意想不到的可能性,即无序和熵必须不断增加。 这是熵力。 为了certificate潜在扩展的所有可能的计算,是先验地计算所有可能的扩展。
这就是停止定理存在的原因,即图灵完全编程语言中每个可能的程序是否终止是不可判定的。 可以certificate,某些特定的程序终止了(所有的可能性已被定义和计算)。 但是不可能certificate该程序的所有可能的扩展终止,除非该程序的扩展的可能性不是图灵完备的(例如通过依赖types)。 由于图灵完备性的基本要求是无界recursion ,直观地理解了哥德尔的不完备性定理和拉塞尔悖论如何适用于扩展。
这些定理的解释将它们纳入对熵力的广义概念性理解中:
- 哥德尔的不完备性定理 :所有算术真理都可以certificate的任何forms理论都是不一致的。
- 罗素的悖论 :一个可以包含一个集合的集合的每个成员规则要么枚举每个成员的具体types,要么包含它自己。 因此集既不能扩展也不能无限recursion。 例如,所有不是茶壶的东西都包括在内,包括自身在内,包括自身等等。 因此,一个规则是不一致的,如果它(可能包含一个集合)不枚举特定types(即允许所有未指定的types),并且不允许无限扩展。 这是不是自己成员的集合。 哥德尔的不完全性定理是不可能一致而且完全列举的。
- Liskovreplace原则 :一般来说,是否有任何一个集合是另一个集合的子集是不可判定的问题,即inheritance通常是不可判定的。
- 林斯基指称 :当描述或感知时,事物的计算是不可判定的,即感知(现实)没有绝对的参照点。
- 科斯定理 :没有外部参照点,因此阻碍无限外部可能性的任何障碍都将失败。
- 热力学的第二定律 :整个宇宙(一个封闭的系统,即一切)趋向最大的无序,即最大的独立可能性。
LSP是一个关于类的契约的规则:如果一个基类满足一个契约,那么由LSP派生类也必须满足该契约。
在伪python
class Base: def Foo(self, arg): # *... do stuff* class Derived(Base): def Foo(self, arg): # *... do stuff*
如果每次在Derived对象上调用Foo,都会满足LSP,只要arg相同,它就会给出与在Base对象上调用Foo完全相同的结果。
使用指针或基类的引用的函数必须能够使用派生类的对象而不知道它。
当我第一次读到LSP的时候,我认为这是一个非常严格的意义,基本上等同于接口实现和types安全的铸造。 这意味着LSP要么由语言本身来保证。 例如,就这个严格的意义而言,就编译器而言,ThreeDBoard当然可以替代Board。
在阅读了更多关于这个概念之后,我发现LSP通常被解释得更广泛。
简而言之,客户端代码“知道”指针背后的对象是派生types而不是指针types的含义并不局限于types安全。 坚持LSP也可以通过探测对象的实际行为来检验。 也就是说,检查对象状态和方法参数对方法调用结果的影响,或者从对象抛出的exceptiontypes。
再回到这个例子, 理论上可以使Board方法在ThreeDBoard上正常工作。 然而,在实践中,要防止客户端可能无法正确处理的行为上的差异是非常困难的,而不会妨碍ThreeDBoard打算添加的function。
有了这些知识,评估LSP依从性可以成为确定何时组合是扩展现有function而不是inheritance的更合适机制的重要工具。
奇怪的是,没有人发表过描述lsp的原始论文 。 这不像罗伯特·马丁那样简单,但值得。
一个使用 LSP的重要例子是软件testing 。
如果我有一个B类的符合LSP的子类,那么我可以重新使用B的testing套件来testingA.
为了完全testing子类A,我可能需要添加更多的testing用例,但至less我可以重用所有超类B的testing用例。
通过构buildMcGregor所说的“testing的并行层次结构”来实现这一点:我的ATest
类将从BTest
inheritance。 然后需要某种forms的注入来确保testing用例能够处理typesA而不是typesB的对象(一个简单的模板方法模式可以)。
请注意,重用所有子类实现的超级testing套件实际上是一种testing这些子类实现是否符合LSP的方法。 因此,人们也可以争辩说, 应该在任何子类的上下文中运行超类testing套件。
另请参阅Stackoverflow问题的答案“ 我可以实现一系列可重用的testing来testing接口的实现吗?
有一个检查清单,以确定你是否违反Liskov。
- 如果您违反以下任何一项 – >您违反了Liskov。
- 如果你不违反任何 – >无法完成任何事情。
清单:
- 在派生类中不应该抛出新的exception :如果你的基类抛出了ArgumentNullException,那么你的子类只允许抛出types为ArgumentNullException的exception或从ArgumentNullException派生的exception。 抛出IndexOutOfRangeException是对Liskov的违反。
- 前提条件不能被强化 :假设你的基类与一个int成员一起工作。 现在你的子types要求int是正数。 这是前提条件的加强,现在任何代码工作完全罚款之前负号整数被打破。
- 后置条件不能被削弱 :假设你的基类要求所有的数据库连接都应该在方法返回前closures。 在你的子类中,你会覆盖这个方法并打开连接,以便进一步重用。 你削弱了该方法的后置条件。
- 不variables必须被保留下来 :实现最困难和最痛苦的约束。 不variables在基类中隐藏了一段时间,揭示它们的唯一方法是读取基类的代码。 基本上你必须确保当你覆盖一个方法时,任何不可改变的东西在你重写的方法执行后必须保持不变。 我能想到的最好的事情是在基类中强制执行这个不变的约束,但这并不容易。
-
历史约束 :重写方法时,不允许修改基类中的不可修改的属性。 看看这些代码,你可以看到Name被定义为不可修改的(私有集合),但SubType引入了新的方法,允许修改它(通过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); } }
还有两个项目: 方法参数的变换和返回types的协方差 。 但在C#中(我是C#开发人员)是不可能的,所以我不在乎它们。
参考:
- http://www.ckode.dk/programming/solid-principles-part-3-liskovs-substitution-principle/
- https://softwareengineering.stackexchange.com/questions/187613/how-does-strengthening-of-pre-conditions-and-weakening-of-post-conditions-violat
- https://softwareengineering.stackexchange.com/questions/170189/how-to-verify-the-liskov-substitution-principle-in-an-inheritance-hierarchy
我想每个人都会在技术上涵盖什么LSP:您基本上希望能够从子types细节中抽象出来,并安全地使用超types。
所以Liskov有三个基本规则:
-
签名规则:在子types中应该有一个超types的每个操作的有效实现。 编译器将能够检查你的东西。 关于抛出更less的exception和至less与超types方法一样可访问有一点规则。
-
方法规则:这些操作的实现在语义上是合理的。
- 较弱的先决条件:子types函数应至less采用超类作为input,如果不是更多。
- 更强的后置条件:他们应该产生超types方法产生的输出的一个子集。
-
属性规则:这超出了单独的函数调用。
- 不variables:事情总是真实的,必须保持真实。 例如。 Set的大小永远不会是负的。
- 进化属性:通常与不变性或对象可以处于的状态有关。或者对象只能增长,永远不缩小,所以子types方法不应该这样做。
所有这些属性都需要保留,额外的子typesfunction不应该违反超types属性。
如果这三件事情都照顾好了,那么你已经从底层的东西中抽象出来了,而你正在编写松散耦合的代码。
资料来源:Java程序开发 – Barbara Liskov
面向对象程序devise中的可替代性是一个原则,说明在计算机程序中,如果S是T的一个子types,那么typesT的对象可以被Stypes的对象
让我们在Java中做一个简单的例子:
不好的例子
public class Bird{ public void fly(){} } public class Duck extends Bird{}
鸭子可以飞,因为它的鸟,但这又如何:
public class Ostrich extends Bird{}
鸵鸟是一只鸟,但它不能飞,鸵鸟类是鸟类的一个亚类,但是它不能使用飞行方法,这意味着我们打破了LSP原理。
很好的例子
public class Bird{ } public class FlyingBirds extends Bird{ public void fly(){} } public class Duck extends FlyingBirds{} public class Ostrich extends Bird{}
LSP的这个表述太强大了:
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.
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.
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:
- In-variants of base class must be preserved by the derived class
- Pre-conditions of the base class must not be strengthened by the derived class
- Post-conditions of the base class must not be weakened by the derived class.
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
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.
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).
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.
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.
LISKOV SUBSTITUTION PRINCIPLE (From Mark Seemann book) states that we should be able to replace one implementation of an interface with another without breaking either client or implementation.It's this principle that enables to address requirements that occur in the future, even if we can't foresee them today.
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.
The Liskov Substitution Principal states that Subtypes must be substitutable for their base types.
Child class must not:
- Remove base class behavior
- Violate base class invariants
I would like to add code example to solve Liskov Substitution Principal
The Problem is
In mathematics, a Square
is a Rectangle
. Indeed it is a specialization of a rectangle. The "is a" makes you want to model this with inheritance. However if in code you made Square
derive from Rectangle
, then a Square
should be usable anywhere you expect a Rectangle
. This makes for some strange behavior.
Imagine you had SetWidth
and SetHeight
methods on your Rectangle
base class; this seems perfectly logical. However if your Rectangle
reference pointed to a Square
, then SetWidth
and SetHeight
doesn't make sense because setting one would change the other to match it. In this case Square
fails the Liskov Substitution Test with Rectangle
and the abstraction of having Square
inherit from Rectangle
is a bad one.
In Order to solve it we have two approaches
- By Using If condition
- By using abstract and override keyword
First approach using If
condition
-
This approach solves
Liskov Substitution Principal
but violatesOpen closed principle
[TestMethod] public void TwentyFor4X5ShapeFromRectangleAnd9For3X3Square() { var shapes = new List<Shape> { new Rectangle {Height = 4, Width = 5}, new Square {SideLength = 3} }; var areas = new List<int>(); foreach (Shape shape in shapes) { if (shape.GetType() == typeof(Rectangle)) { areas.Add(((Rectangle)shape).Area()); } if (shape.GetType() == typeof(Square)) { areas.Add(((Square)shape).Area()); } } Assert.AreEqual(20, areas[0]); Assert.AreEqual(9, areas[1]); } public class Rectangle : Shape { public int Height { get; set; } public int Width { get; set; } public int Area() { return Height * Width; } } public abstract class Shape { } public class Square : Shape { public int SideLength; public int Area() { return SideLength * SideLength; } }
Second and best approach by using abstract
and override
[TestMethod] public void TwentyFor4X5ShapeFromRectangleAnd9For3X3Square() { var shapes = new List<Shape> { new Rectangle {Height = 4, Width = 5}, new Square {SideLength = 3} }; var areas = new List<int>(); foreach (Shape shape in shapes) { areas.Add(shape.Area()); } Assert.AreEqual(20, areas[0]); Assert.AreEqual(9, areas[1]); } public abstract class Shape { public abstract int Area(); } public class Rectangle : Shape { public int Height { get; set; } public int Width { get; set; } public override int Area() { return Height*Width; } } public class Square : Shape { public int SideLength; public override int Area() { return SideLength*SideLength; } }
The benefit of following Liskov Substitution Principal is it allows for proper use of polymorphism and produces more maintainable code.
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.
Likov's Substitution Principle states that if a program module is using a Base class, then the reference to the Base class can be replaced with a Derived class without affecting the functionality of the program module.
Intent – Derived types must be completely substitute able for their base types.
Example – Co-variant return types in java.
Let's say we use a rectangle in our code
r = new Rectangle(); // ... r.setDimensions(1,2); r.fill(colors.red()); canvas.draw(r);
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:
class Square extends Rectangle { setDimensions(width, height){ assert(width == height); super.setDimensions(width, height); } }
If we replace the Rectangle
with Square
in our first code, then it will break:
r = new Square(); // ... r.setDimensions(1,2); // assertion width == height failed r.fill(colors.red()); canvas.draw(r);
This is because the Square
has a new precondition we did not have in the Rectangle
class: width == height
. According to LSP the Rectangle
instances should be substitutable with Rectangle
subclass instances. This is because these instances pass the type check for Rectangle
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.
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.
class ItemsRepository { /** * @return int Returns number of deleted rows */ public function delete() { // perform a delete query $numberOfDeletedRows = 10; return $numberOfDeletedRows; } }
And a sub class extending it:
class BadlyExtendedItemsRepository extends ItemsRepository { /** * @return void Was suppose to return an INT like parent, but did not, breaks LSP */ public function delete() { // perform a delete query $numberOfDeletedRows = 10; // we broke the behaviour of the parent class return; } }
Then you could have a Client working with the Base ItemsRepository API and relying on it.
/** * Class ItemsService is a client for public ItemsRepository "API" (the public delete method). * * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository * but if the sub-class won't abide the base class API, the client will get broken. */ class ItemsService { /** * @var ItemsRepository */ private $itemsRepository; /** * @param ItemsRepository $itemsRepository */ public function __construct(ItemsRepository $itemsRepository) { $this->itemsRepository = $itemsRepository; } /** * !!! Notice how this is suppose to return an int. My clients expect it based on the * ItemsRepository API in the constructor !!! * * @return int */ public function delete() { return $this->itemsRepository->delete(); } }
The LSP is broken when substituting parent class with a sub class breaks the API's contract .
class ItemsController { /** * Valid delete action when using the base class. */ public function validDeleteAction() { $itemsService = new ItemsService(new ItemsRepository()); $numberOfDeletedItems = $itemsService->delete(); // $numberOfDeletedItems is an INT :) } /** * Invalid delete action when using a subclass. */ public function brokenDeleteAction() { $itemsService = new ItemsService(new BadlyExtendedItemsRepository()); $numberOfDeletedItems = $itemsService->delete(); // $numberOfDeletedItems is a NULL :( } }