当前位置:  开发笔记 > 编程语言 > 正文

Liskov替代原则的例子是什么?

如何解决《Liskov替代原则的例子是什么?》经验,为你挑选了18个好方法。

我听说Liskov替换原则(LSP)是面向对象设计的基本原则.它是什么以及它的使用例子是什么?



1> m-sharp..:

一个很好的例子说明了LSP(我最近在一个播客中由Bob叔叔给出的)是有时在自然语言中听起来不对的东西在代码中不起作用.

在数学中,a Square是a Rectangle.实际上它是一个矩形的专业化."是一个"让你想要继承模型.但是,如果你在代码中Square派生出来Rectangle,那么a Square应该可以在任何你期望的地方使用Rectangle.这会产生一些奇怪的行为.

想象一下你有基类的方法SetWidthSetHeight方法Rectangle; 这似乎完全符合逻辑.但是,如果你Rectangle参考指着一个Square,然后SetWidthSetHeight没有意义,因为设置一个将改变其他与之相匹配的.在这种情况下SquareLiskov替换测试失败Rectangle并且Square继承的抽象Rectangle是一个坏的.

你们应该查看其他无价的SOLID原理励志海报.


嗯,正方形显然是现实世界中的一种矩形.我们是否可以在代码中对此进行建模取决于规范.LSP指示的是子类型行为应该与基本类型规范中定义的基类型行为匹配.如果矩形基本类型规范说高度和宽度可以独立设置,那么LSP表示square不能是矩形的子类型.如果矩形规格说矩形是不可变的,那么正方形可以是矩形的子类型.这是关于维护为基本类型指定的行为的子类型.
故事的道德:根据不属性的行为来模拟你的课程; 根据属性而不是行为来建模数据.如果它像鸭子一样,它肯定是一只鸟.
@Pacerier如果它是不可变的则没有问题.这里真正的问题是我们不是建模矩形,而是"可重新构造的矩形",即在创建后可以修改其宽度或高度的矩形(我们仍然认为它是同一个对象).如果我们以这种方式查看矩形类,很明显,正方形不是"可重新形成的矩形",因为正方形不能被重新整形并且仍然是正方形(通常).在数学上,我们没有看到问题,因为可变性在数学上下文中甚至没有意义.
@ m-sharp如果它是一个不可变的Rectangle,而不是SetWidth和SetHeight,我们有GetWidth和GetHeight方法吗?
我对这个原则有一个疑问.如果像这样实现`Square.setWidth(int width)`,为什么会出现问题:`this.width = width; this.height = width;`?在这种情况下,保证宽度等于高度.
然而,图片并不是破损LSP的有效例子(实际上是Reddit的气味,这不是我们想在SO上闻到的东西).子类总是比超类更具体.但是,它们的细节不一定会破坏LSP.这些细节是否影响(和破坏)共同合同是一个问题.电池供电的鸭子是否打破了抽象鸭子的普通合同取决于设计的具体细节.
从[Barbara Liskov教授的讲座](http://youtu.be/dtZ-o96bH9A?t=40m):"如果通过超类型方法使用,子类型的对象应该像超类型的对象一样."
似乎没有回答这个问题.阅读之后,我仍然不知道LSP是什么(除非海报包含定义,但考虑到这些海报的背景,这一点并不清楚).
我也没有得到方形的例子.setWidth和setHeight如何在正方形上"有意义".正如你很容易解释的那样,做一个暗示另一个 - 这只是一个正方形的定义.它没有解释为什么用矩形代替矩形有什么不对或不一致.
难道你不能简单地覆盖setWidth或setHeight,并且在定义中使用相同的参数和poof调用另一个方法,它的所有工作和行为是否一致?
@MCEmperor如果你在`Square`中更改`setHeight()`和`setWidth()`的实现,那么你使用`Rectangule`的代码中的地方将不再有效,如果你通过`Square`和这是关于LSP的要点;
`o.setDimensions(width,height)`可以解决这个问题,但LSP仍会被违反,因为正方形比矩形具有更强的前提条件(`width == height`).我认为你的帖子没有回答关于LSP的任何内容.
+1是因为最后的动机!
如果设置员可以更改宽度和高度,则应该只有一个矩形类,而没有特殊的正方形类。相反,矩形类应具有名为IsSquare的吸气剂。每当width和height具有相同的值时,IsSquare将返回true,否则返回false。类型并不总是静态的,但是有时(在这种情况下)可以更改。
那个励志海报毫无意义.它应该是"如果它看起来像鸭子,嘎嘎叫鸭子,但需要电池......为什么你又对电池感兴趣?" 因为最后它暗示鸭子的唯一替代品是另一只鸭子(最好是完全相同的鸭子).这种激进主义是弄巧成拙的.
另一种解决方案是将矩形定义为保留长宽比,而不是具有独立的宽度和高度。
每个人都使用这个方形vs矩形这样一个可怕的例子.您将覆盖Width属性以设置高度属性,这似乎更像是副作用.如果您让人们明确设置宽度和高度,这一切都将得到解决.
@AustinWBryan不,这些是定义明确的数量.正方形有高度和宽度; 他们碰巧是平等的.如果读者被这个抛弃了,那么他们要么学习一些新的和重要的代码,要么他们不属于那里:)

2> NotMyself..:

Liskov替换原则(LSP,lsp)是面向对象编程中的一个概念,它指出:

使用指针或对基类的引用的函数必须能够在不知道它的情况下使用派生类的对象.

从本质上讲,LSP是关于接口和契约以及如何决定何时扩展一个类而不是使用另一个策略(如组合)来实现您的目标.

我所看到的最有效的方式就是Head First OOA&D.他们提出了一个场景,您是一个项目开发人员,为战略游戏构建框架.

他们提出了一个代表董事会的类,如下所示:

类图

所有方法都将X和Y坐标作为参数来定位二维数组中的tile位置Tiles.这将允许游戏开发者在游戏过程中管理棋盘中的单元.

这本书继续改变要求,说游戏框架工作也必须支持3D游戏板,以适应有飞行的游戏.因此ThreeDBoard引入了一个扩展的类Board.

乍一看,这似乎是一个很好的决定.Board提供HeightWidth属性并ThreeDBoard提供Z轴.

它崩溃的地方是你看看继承自的所有其他成员Board.对于这些方法AddUnit,GetTile,GetUnits等等,都需要在X和Y参数Board类,但ThreeDBoard需要一个参数Z为好.

因此,您必须使用Z参数再次实现这些方法.Z参数没有Board类的上下文,类中继承的方法Board失去了意义.试图使用ThreeDBoard该类作为其基类的代码单元Board将非常不幸.

也许我们应该找到另一种方法.而不是扩展Board,ThreeDBoard应该由Board对象组成.Board每单位Z轴一个对象.

这允许我们使用良好的面向对象原则,如封装和重用,并且不违反LSP.


另请参阅Wikipedia上的[Circle-Ellipse Problem](http://en.wikipedia.org/wiki/Circle-ellipse_problem),以获得类似但更简单的示例.
这是一个反Liskov的例子.利斯科夫让我们从广场派生出矩形.来自less-parameters-class的More-parameters-class.你已经很好地表明它很糟糕.将liskov问题作为反liskov答案的200次投票真的是一个很好的笑话.Liskov原则真的是一个谬误吗?
我看到继承工作的方式不对.这是一个例子.基类应该是3DBoard和派生类Board.电路板仍然具有Max(Z)= Min(Z)= 1的Z轴

3> Konrad Rudol..:

LSP涉及不变量.

下面的伪代码声明给出了经典示例(省略了实现):

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

现在我们遇到了一个问题,尽管界面匹配.原因是我们违反了由正方形和矩形的数学定义产生的不变量.getter和setter的工作方式,a Rectangle应满足以下不变量:

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

但是,必须通过正确的实现来违反此不变量Square,因此它不是有效的替代Rectangle.


因此难以使用"OO"来模拟我们可能想要实际建模的任何东西.
@DrPizza:绝对.但是,有两件事.首先,这些关系可以*仍然*在OOP中建模,虽然不完整或使用更复杂的弯路(选择适合您的问题).其次,没有更好的选择.其他映射/建模具有相同或类似的问题.;-)
OOP旨在模拟行为而不是数据.即使在违反LSP之前,您的类也会违反封装.
@NickW在某些情况下(但不是在上面)你可以简单地反转继承链 - 从逻辑上讲,2D点是一个3D点,其中第三维被忽略(或者0 - 所有点都位于同一平面上) 3D空间).但这当然不是很实际.通常,这是继承实际上没有帮助的情况之一,并且实体之间不存在自然关系.分别建模(至少我不知道更好的方法).
@AustinWBryan Yep; 我在该领域工作的时间越长,我越倾向于将继承仅用于接口和抽象基类,而将组合用于其余部分。有时需要做更多的工作(明智地键入内容),但是它避免了很多问题,并且被其他经验丰富的程序员广泛建议。

4> Maysara Alhi..:

可替代性是面向对象编程中的一个原则,表明在计算机程序中,如果S是T的子类型,则类型T的对象可以用类型S的对象替换

让我们用Java做一个简单的例子:

不好的例子

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

鸭子可以飞,因为它是一只鸟,但是这个怎么样:

public class Ostrich extends Bird{}

鸵鸟是一只鸟,但它不能飞,鸵鸟类是Bird类的子类,但它不能使用fly方法,这意味着我们正在打破LSP原理.

好例子

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 


不.如果客户端有"鸟鸟",那就意味着它不能使用`fly()`.而已.传递'鸭子'不会改变这个事实.如果客户端有`FlyingBirds bird`,那么即使它传递了一个`Duck`,它应该总是以相同的方式工作.
这还不是接口隔离的一个很好的例子吗?
很好的例子,但如果客户有"鸟鸟",你会怎么做?你必须将对象强制转换为FlyingBirds才能使用fly,这不是很好吗?
如何使用接口“ Flyable”(想不出更好的名字)。这样,我们就不会将自己投入严格的层次结构中。除非我们知道确实需要它。

5> Phillip Well..:

罗伯特马丁有一篇关于利斯科夫替代原则的优秀论文.它讨论了可能违反原则的微妙而不那么微妙的方式.

本文的一些相关部分(注意第二个例子是高度浓缩的):

一个违反LSP的简单例子

最明显违反此原则的行为之一是使用C++运行时类型信息(RTTI)根据对象的类型选择函数.即:

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast(s));
}

很明显,这个DrawShape功能很糟糕.它必须知道Shape类的每个可能的衍生物,并且必须在Shape创建新的衍生物时进行更改.实际上,许多人认为这个函数的结构是面向对象设计的诅咒.

正方形和矩形,更微妙的违规.

然而,还有其他更微妙的违反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 类建模为派生自然是合乎逻辑的Rectangle.[...]

Square将继承SetWidthSetHeight功能.这些功能完全不适合a Square,因为正方形的宽度和高度是相同的.这应该是设计存在问题的重要线索.但是,有一种方法可以回避这个问题.我们可以覆盖SetWidthSetHeight[...]

但请考虑以下功能:

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

如果我们将Square对象的引用传递给此函数,则该 Square对象将被破坏,因为高度不会更改.这显然违反了LSP.该函数不适用于其参数的派生.

[...]


迟到了,但我认为这篇文章中有一个有趣的引用:"现在Meyer所说的衍生物的先决条件和后置条件的规则是:......当重新定义[衍生]中的例程时,你只能更换一个较弱者的前提条件,一个较强者的后置条件.如果一个孩子级的前提条件强于父母类前提条件,你就不能在没有违反前提条件的情况下将孩子替换为父母. .因此LSP.

6> Shelby Moore..:

LSP是必要的,其中某些代码认为它正在调用类型的方法T,并且可能在不知不觉中调用类型的方法S,其中S extends T(即S继承,派生自或是超类型的子类型T).

例如,这种情况发生在具有类型的输入参数的函数T被调用(即调用)且参数值为type的情况下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

即使是具有不可变字段的类型仍然具有不变量,例如,不可变的 Rectangle设置器期望维度被独立修改,但是不可变的 Square setter违反了这种期望.

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必须具有逆变输入参数和协变输出.

逆变意味着方差与继承的方向相反,即子类型Si的每个方法的每个输入参数的类型S必须相同或类型Ti的相应方法的相应输入参数的类型的超类型T.

协方差意味着方差与继承的方向相同,即子类型So的每个方法的输出的类型S必须是相同的或超类型的相应方法的相应输出的类型的To类型T.

这是因为如果调用者认为它有一个类型T,认为它正在调用一个方法T,那么它提供类型的参数Ti并将输出分配给该类型To.当它实际调用相应的方法时S,则将每个Ti输入参数分配给Si输入参数,并将So输出分配给该类型To.因此,如果Si不是逆变的话Ti,则可以分配一个子类型Xi- 它不是 - 的子类型.SiTi

此外,对于在类型多态性参数(即泛型)上具有定义 - 位置方差注释的语言(例如Scala或Ceylon),类型的每个类型参数的方差注释的共同或反方向T必须相反或相同方向分别对T具有类型参数类型的每个输入参数或输出(每个方法).

另外,对于具有功能类型的每个输入参数或输出,所需的方差方向相反.此规则以递归方式应用.


在枚举不变量的情况下,子类型是合适的.

关于如何建模不变量的研究正在进行中,因此它们由编译器强制执行.

Typestate(参见第3页)声明并强制执行与类型正交的状态不变量.或者,可以通过将断言转换为类型来强制执行不变量.例如,要在关闭文件之前声明文件已打开,则File.open()可以返回OpenFile类型,该类型包含File中不可用的close()方法.甲井字棋API可以是采用打字执行在编译时不变的另一个例子.类型系统甚至可能是图灵完备的,例如Scala.依赖类型语言和定理证明器使高阶类型的模型形式化.

由于需要语义来抽象扩展,我希望使用键入来模拟不变量,即统一的高阶指称语义,优于Typestate."扩展"是指无协调,模块化开发的无限制,置换组合.因为在我看来,它是统一的对立面,因而是自由度,有两个相互依赖的模型(例如类型和类型状态)来表达共享语义,这些模型不能相互统一以实现可扩展的组合.例如,类似表达式问题的扩展在子类型,函数重载和参数类型域中统一.

我的理论立场是,为了存在知识(参见"集中化是盲目和不合适的"一节),永远不会有一个通用模型能够在图灵完备的计算机语言中强制实现100%覆盖所有可能的不变量.为了存在知识,存在着意想不到的可能性,即无序和熵必须一直在增加.这是熵力.为了证明潜在扩展的所有可能计算,是计算先验所有可能的扩展.

这就是Halting定理存在的原因,即图灵完备编程语言中的每个可能的程序是否终止都是不可判定的.可以证明某些特定程序终止(所有可能性都已定义和计算).但是不可能证明该程序的所有可能扩展都终止,除非扩展该程序的可能性不是图灵完成(例如通过依赖类型).由于图灵完备性的基本要求是无界递归,因此直观地理解哥德尔的不完备性定理和罗素的悖论如何适用于扩展.

对这些定理的解释将它们纳入对熵力的概括性概念理解中:

哥德尔的不完备性定理:任何可以证明所有算术真理的形式理论都是不一致的.

Russell悖论:可以包含集合的集合的每个成员规则,可以枚举每个成员的特定类型或包含自身.因此集合既不能扩展也不能无限递归.例如,一套不是茶壶的东西,包括它自己,包括它自己,包括它自己等等.因此,如果规则(可能包含集合)不枚举特定类型(即允许所有未指定的类型)并且不允许无限扩展,则规则是不一致的.这是一组不属于自己的集合.哥德尔的不完备性定理是无法在所有可能的扩展中保持一致和完全列举的.

Liskov Substition Principle:通常它是一个不可判定的问题,无论任何集合是否是另一个集合的子集,即继承通常是不可判定的.

Linsky Referencing:在描述或感知事物时,某事物的计算是不可判定的,即感知(现实)没有绝对的参照点.

科斯定理:没有外部参考点,因此无限外部可能性的任何障碍都将失败.

热力学第二定律:整个宇宙(一个封闭的系统,即一切)趋向于最大程度的无序,即最大的独立可能性.


@Shelyby:你混合了很多东西.事情并不像你说的那样令人困惑.你的许多理论断言都是基于脆弱的理由,比如"为了知识存在,意外的可能性存在很多,......"和"通常它是一个不可判定的问题,无论任何集合是否是另一个集合的子集,即继承通常是不可判定的'.您可以为每个点启动单独的博客.无论如何,你的断言和假设是非常值得怀疑的.一个人不能使用人们不知道的东西!
@ShelbyMooreIII你的方向太多了.这不是答案.

7> Chris Ammerm..:

使用指针或对基类的引用的函数必须能够在不知道它的情况下使用派生类的对象.

当我第一次阅读LSP时,我认为这是非常严格意义上的,基本上将其等同于接口实现和类型安全转换.这意味着LSP要么由语言本身确保,要么得到保证.例如,在严格意义上,就编译器而言,ThreeDBoard肯定可替代Board.

在阅读了更多关于这个概念之后,我发现LSP通常比这更广泛地被解释.

简而言之,客户端代码"知道"指针后面的对象是派生类型而不是指针类型意味着什么并不限于类型安全.通过探测对象的实际行为,也可以测试对LSP的遵守情况.也就是说,检查对象的状态和方法参数对方法调用结果的影响,或者从对象抛出的异常类型.

再回到示例,理论上可以使Board方法在ThreeDBoard上正常工作.然而,在实践中,很难防止客户端可能无法正确处理的行为差异,而不会影响ThreeDBoard要添加的功能.

掌握了这些知识,评估LSP依从性可以成为确定何时组合是扩展现有功能而不是继承的更合适机制的一个很好的工具.



8> Charlie Mart..:

LSP是关于clases合同的规则:如果基类满足合同,那么LSP派生类也必须满足该合同.

在Pseudo-python中

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

如果每次在Derived对象上调用Foo时,它都会满足LSP,它给出的结果与在Base对象上调用Foo完全相同,只要arg是相同的.


但是......如果你总是得到相同的行为,那么获得派生类有什么意义呢?

9> Cù Đức Hiếu..:

有一个检查清单,以确定您是否违反Liskov.

如果您违反以下任何一项 - >您违反了Liskov.

如果你不违反任何 - >无法做出任何结论.

清单:

在派生类中不应抛出新的异常:如果您的基类抛出了ArgumentNullException,那么您的子类只允许抛出ArgumentNullException类型的异常或从ArgumentNullException派生的任何异常.抛出IndexOutOfRangeException是违反Liskov的.

前提条件无法加强:假设您的基类使用成员int.现在你的子类型要求int为正数.这是强化前置条件,现在任何在使用负数之前完全正常工作的代码都会被破坏.

后置条件不能被削弱:假设您的基类需要在返回方法之前关闭与数据库的所有连接.在您的子类中,您覆盖该方法并保持连接打开以便进一步重用.你已经削弱了该方法的后置条件.

必须保留不变量:要实现的最困难和痛苦的约束.不变量有时隐藏在基类中,揭示它们的唯一方法是读取基类的代码.基本上,您必须确保在重写方法时,在执行重写方法后,任何不可更改的内容都必须保持不变.我能想到的最好的事情是在基类中强制执行这种不变约束,但这并不容易.

历史约束:覆盖方法时,不允许修改基类中的不可修改属性.看看这些代码,您可以看到Name被定义为不可修改(私有集),但SubType引入了允许修改它的新方法(通过反射):

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#开发人员)所以我不关心他们.

参考:

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



10> avandeursen..:

使用 LSP的一个重要例子是软件测试.

如果我有一个类A是符合LSP的B类子类,那么我可以重用B的测试套件来测试A.

为了完全测试子类A,我可能需要添加一些测试用例,但至少我可以重用所有超类B的测试用例.

一种方法是通过构建McGregor所谓的"并行层次结构进行测试"来实现这一点:我的ATest类将继承自BTest.然后需要某种形式的注入来确保测试用例适用于类型A而不是类型B的对象(简单的模板方法模式将会这样做).

请注意,为所有子类实现重用超级测试套件实际上是一种测试这些子类实现是否符合LSP的方法.因此,人们也可以争辩说,应该在任何子类的上下文中运行超类测试套件.

另请参阅Stackoverflow问题的答案" 我可以实现一系列可重用的测试来测试接口的实现吗? "



11> 小智..:

我想每个人都在技术上涵盖了LSP:你基本上希望能够从子类型细节中抽象出来并安全地使用超类型.

所以Liskov有3个基本规则:

    签名规则:语法中子类型中的超类型的每个操作都应该有效实现.编译器可以为您检查的东西.关于抛出更少的异常并且至少像超类型方法一样可访问,有一个小规则.

    方法规则:这些操作的实现在语义上是合理的.

    较弱的前提条件:子类型函数应至少采用超类型作为输入的内容,如果不是更多.

    更强的后置条件:它们应该生成超类型方法产生的输出的子集.

    属性规则:这超出了单个函数调用.

    不变量:始终如一的事物必须保持真实.例如.Set的大小永远不会消极.

    进化属性:通常与不变性或对象可以处于的状态有关.或者对象只能增长而不会缩小,因此子类型方法不应该成功.

需要保留所有这些属性,并且额外的子类型功能不应违反超类型属性.

如果这三件事都得到了解决,那么你已经从基础内容中抽象出来了,并且你正在编写松散耦合的代码.

资料来源:Java项目开发 - Barbara Liskov



12> Steve Chamai..:

我在每个答案中看到矩形和正方形,以及如何违反LSP.

我想展示如何通过一个真实世界的例子来符合LSP:



此设计符合LSP,因为无论我们选择使用哪种实现,行为都保持不变.

是的,您可以在此配置中违反LSP,执行一个简单的更改,如下所示:

 $result]; // This violates LSP !
    }
}

现在,子类型不能以相同的方式使用,因为它们不再产生相同的结果.


只要我们将"Database :: selectQuery"的语义限制为仅支持**all**DB引擎支持的SQL子集,该示例就不会违反LSP.这几乎不实用......也就是说,这个例子比其他大多数人都更容易掌握.

13> BlocksByLuka..:

的故事,总之,让延伸的父类,当我们离开矩形长方形和正方形广场,实际的例子,你必须要么保留确切父API或把它扩大.

假设您有一个基于 ItemsRepository 的基础.

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

还有一个扩展它的子类:

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;
    }
}

然后,您可以让客户端使用Base ItemsRepository API并依赖它.

/**
 * 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();
    }
} 

当用子类替换类时,LSP会被破坏API的合同.

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 :(
    }
}

您可以在我的课程中了解有关编写可维护软件的更多信息:https://www.udemy.com/enterprise-php/



14> Damien Polle..:

LSP的这种表述过于强烈:

如果对于类型S的每个对象o1,存在类型为T的对象o2,使得对于根据T定义的所有程序P,当o1代替o2时P的行为不变,则S是T的子类型.

这基本上意味着S是另一个完全封装的与T完全相同的实现.我可以大胆并决定性能是P的行为的一部分......

所以,基本上,任何后期绑定的使用都违反了LSP.当我们将一种对象替换为另一种对象时,获得不同的行为是OO的重点!

维基百科引用的公式更好,因为该属性取决于上下文,并不一定包括该程序的整个行为.


"行为未改变"并不意味着子类型将为您提供完全相同的具体结果值.这意味着子类型的行为与基本类型中的预期相匹配.示例:基本类型Shape可以具有draw()方法,并规定此方法应呈现形状.Shape的两个子类型(例如Square和Circle)都将实现draw()方法,结果看起来会有所不同.但只要行为(渲染形状)与Shape的指定行为匹配,则Square和Circle将根据LSP成为Shape的子类型.
嗯,这个表述是Barbara Liskov自己的.Barbara Liskov,"数据抽象和层次结构",SIGPLAN Notices,23,5,1988年5月.它不是"太强大",它"完全正确",它没有你认为它具有的含义.它很强大,但具有适量的力量.

15> 小智..:

里斯科夫的替代原理(LSP)

一直以来,我们都设计一个程序模块,并创建一些类层次结构。然后我们扩展一些类,创建一些派生类。

我们必须确保新的派生类可以扩展而不替换旧类的功能。否则,当新类在现有程序模块中使用时,可能会产生不希望的效果。

Liskov的“替换原理”指出,如果程序模块使用Base类,则可以用Derived类替换对Base类的引用,而不会影响程序模块的功能。

例:

以下是违反Liskov替代原则的经典示例。在示例中,使用了2个类:矩形和正方形。假设Rectangle对象在应用程序中的某处使用。我们扩展应用程序并添加Square类。根据某些条件,正方形类是通过工厂模式返回的,我们不知道将返回哪种类型的对象。但是我们知道这是一个矩形。我们得到矩形对象,将宽度设置为5,将高度设置为10,并获得面积。对于宽度为5,高度为10的矩形,面积应为50。结果将为100

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

结论:

该原理只是“打开关闭”原理的扩展,它意味着我们必须确保新的派生类在不改变其行为的情况下扩展了基类。

另请参阅:开闭原理

一些类似的概念可以改善结构:约定优于配置



16> 小智..:

让我们用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()方法。

这些都是违反Liskov替代原理而导致的问题,通常可以通过什么都不做甚至无法实现的方法来识别。

这些问题的解决方案是正确的继承层次结构,在我们的案例中,我们将通过区分带有和不带有引擎的运输设备的类别来解决该问题。即使自行车是运输工具,它也没有引擎。在此示例中,我们对运输设备的定义是错误的。它不应该有引擎。

我们可以如下重构我们的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扩展为电动设备。在这里添加引擎对象更为合适。

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

因此,我们的汽车课在遵循Liskov替代原则的同时变得更加专业。

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

我们的自行车课也符合《里斯科夫换人原则》。

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}



17> aknon..:

一些附录:
我想知道为什么没有人写关于派生类必须遵守的基类的不变量,前置条件和后置条件.要使派生类D完全可由Base类B维护,D类必须遵守某些条件:

必须由派生类保留基类的in-variants

派生类不得强化基类的前提条件

派生类不能削弱基类的后置条件.

因此派生必须知道基类强加的上述三个条件.因此,子类型的规则是预先确定的.这意味着,只有当子类型遵守某些规则时,才应遵守"IS A"关系.这些规则以不变量,预先准备和后置条件的形式,应由正式的" 设计合同 " 决定.

关于这方面的进一步讨论可以在我的博客上找到:Liskov Substitution原则



18> Alireza Rahm..:

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

子类不得违反其基类特征.它必须有能力.我们可以说它与子类型相同.

推荐阅读
ar_wen2402851455
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有