利斯科夫替代原理的一个例子是什么?

2020-08-05 18:24:15

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

一个很好的例子说明了LSP(由Bob叔叔在我最近听到的一个播客中给出的)是,有时在自然语言中听起来正确的东西在代码中并不能很好地工作。

在数学中,正方形是一个矩形。实际上,它是矩形的特色化。<#34;>是<#34;>使您希望使用继承对其进行建模。但是,如果在代码中让Square派生自Rectangle,那么Square应该可以在任何需要矩形的地方使用。这导致了一些奇怪的行为。

假设您的Rectangle基类上有SetWidth和SetHeight方法;这似乎完全符合逻辑。但是,如果您的矩形引用指向Square,那么SetWidth和SetHeight就没有意义了,因为设置一个会更改另一个以匹配它。在这种情况下,Square没有通过矩形的Liskov替换测试,而Square从Rectangle继承的抽象是一个糟糕的抽象。

@m-Sharp如果它是一个不变的矩形,而不是SetWidth和SetHeight,而是GetWidth和GetHeight方法呢?--Pacerier。

这个故事的寓意是:基于行为而不是属性为您的类建模;基于属性而不是行为为您的数据建模。如果它的行为像一只鸭子,那它肯定是一只鸟。-Sklivvz。

嗯,正方形显然是现实世界中的一种矩形。我们能否在代码中对此进行建模取决于规范。LSP指示的是子类型行为应该与基本类型规范中定义的基本类型行为相匹配。如果Rectangle基类型规范说高度和宽度可以单独设置,那么LSP就会说Square不能是Rectangle的子类型。如果Rectangle规范说Rectangle是不可变的,那么正方形可以是Rectangle的子类型。这都是关于子类型维护为基类型指定的行为。-SteveT。

@Pacerier如果它是不变的,就没有问题。这里真正的问题是,我们建模的不是矩形,而是可调整形状的矩形,即宽度或高度在创建后可以修改的矩形(我们仍然认为它是同一个对象)。如果我们以这种方式看待Rectangle类,显然正方形不是可重塑的矩形,因为正方形不能被重塑并且(通常)仍然是正方形。从数学上讲,我们看不到这个问题,因为可变性在数学上下文中甚至没有意义。-阿斯迈勒(Masmeurer)

我有一个关于原则的问题。如果Square.setWidth(Int Width)是这样实现的,为什么会有问题:this.width=width;this.high=width;?在这种情况下,可以保证宽度等于高度。-MMC皇帝。

利斯科夫替换原理(LSP,)是面向对象编程中的一个概念,它声明:

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

LSP的核心是关于接口和契约,以及如何决定何时扩展类,而不是使用另一种策略(如组合)来实现您的目标。

我所见过的最有效的说明这一点的方法是在head first OOA&and D中。它们提供了这样一个场景:你是一个项目的开发人员,为战略游戏构建一个框架。

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

这本书接着改变了要求,说游戏框架还必须支持3D游戏板,以适应具有飞行功能的游戏。因此引入了一个扩展Board的ThreeDBoard类。

乍一看,这似乎是个不错的决定。Board提供高度和宽度属性,ThreeDboard提供Z轴。

当你看看从董事会继承的所有其他成员时,它就会被分解。AddUnit、GetTile、GetUnits等的方法都采用Board类中的X和Y参数,但ThreeDBoard也需要Z参数。

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

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

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

从@NotMySelf:";重新引用,我认为这个示例只是为了演示从board继承在ThreeDBoard的上下文中没有意义,并且所有的方法签名对于Z轴都是没有意义的。";。--康戈尔舞曲。

那么,如果我们向Child类添加另一个方法,但是在Child类中Parent的所有功能仍然有意义,这会破坏LSP吗?一方面,我们稍微修改了使用Child的接口,另一方面,如果我们将Child向上强制转换为父级,那么期望父级的代码就可以正常工作。--尼克·孔德拉特耶夫(Nickolay Kondratyev)。

这是一个反利斯科夫的例子。利斯科夫让我们从正方形推导出矩形。More-Parameters-Class来自Less-Parameters-Class。你已经很好地证明了这是不好的。这真是一个很好的笑话,被标记为一个答案,并在利斯科夫的问题上被评为反利斯科夫的答案200次。利斯科夫原理真的是谬论吗?--冈努斯。

我见过继承以错误的方式进行。这里有一个例子。基类应该是3DBoard和派生类Board。董事会的Z轴仍然是Max(Z)=Min(Z)=1--Paulustrious

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

鸵鸟是一种鸟,但它不会飞,鸵鸟类是鸟类的一个子类,但它不能使用Fly方法,这意味着我们违反了LSP原则。

公共类Bird{}公共类FlyingBirds扩展Bird{public void Fly(){}}公共类Duck扩展FlyingBirds{}公共类鸵鸟扩展Bird{}。

很好的例子,但是如果客户有Bird bird,你会怎么做呢?你必须将物体投掷给FlyingBirds才能使用Fly,这不是很好吗?--穆迪。

不是的。如果客户端有Bird bird,这意味着它不能使用Fly()。就是这样。越过一只鸭子并不能改变这一事实。如果客户端有FlyingBirds鸟,那么即使它被传递给一只鸭子,它也应该始终以相同的方式工作。-史蒂夫·查迈拉德(Steve Chamaillard)。

使用Interface';flyable';怎么样(想不出更好的名字了)。这样我们就不会把自己投入到这个僵化的等级制度中。除非我们知道真的需要它。-约翰·瑟迪(Thirdy)。

Class Rectangle{int getHeight()void setHeight(Int Value)int getWidth()void setWidth(Int Value)}class Square:Rectangle{}。

现在我们遇到了一个问题,尽管接口匹配。原因是我们违反了正方形和矩形的数学定义中的不变量。按照getter和setter的工作方式,矩形应满足以下不变量:

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

因此,使用OO&34;对我们可能想要实际建模的任何东西进行建模都很困难。-DrPizza。

@DrPizza:当然可以。然而,有两件事。首先,这样的关系仍然可以在OOP中建模,尽管不完全或使用更复杂的弯路(选择适合您问题的)。其次,没有更好的选择。其他映射/建模也有相同或类似的问题。;-)-康拉德·鲁道夫(Konrad Rudolph)。

@NickW在某些情况下(但不是在上面),您可以简单地反转继承链-从逻辑上讲,2D点是-3D点,其中第三维被忽略(或0-所有点位于3D空间中的同一平面上)。但这当然不切实际。一般来说,这是继承没有真正帮助的情况之一,并且实体之间不存在自然关系。分别为它们建模(至少我不知道有更好的方法)。--康拉德·鲁道夫(Konrad Rudolph)。

OOP的目的是建模行为,而不是数据。您的类甚至在违反LSP之前就违反了封装。-Sklivvz。

@AustinWBryan Yep;我在这个领域工作的时间越长,我就越倾向于只对接口和抽象基类使用继承,而对其余类使用组合。它有时需要更多的工作(打字明智),但是它避免了一大堆问题,并且得到了其他有经验的程序员的广泛响应。--康拉德·鲁道夫(Konrad Rudolph)。

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

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

Void DrawShape(常量形状){if(typeid(S)==typeid(正方形))DrawSquare(static_cast<;Square&;>;(s));Else if(typeid(S)==typeid(圆形))DrawCircle(static_cast<;Circle&;>;(s));}。

显然,DrawShape函数的格式不正确。它必须知道Shape类的每个可能的派生,并且每当创建Shape的新派生时都必须更改它。事实上,许多人认为这个函数的结构是面向对象设计的诅咒。

然而,还有其他更微妙的违反LSP的方式。考虑一个使用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将继承SetWidth和SetHeight函数。这些函数对于正方形来说是完全不合适的,因为正方形的宽度和高度是相同的。这应该是设计有问题的重要线索。然而,有一种方法可以回避这个问题。我们可以覆盖SetWidth和SetHeight[...]。

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

太晚了,但我认为这是那篇论文中有趣的一句话:现在,正如迈耶所说的那样,衍生品的前置条件和后置条件的规则是:……当重新定义[在衍生品中]的例程时,你只能用较弱的前置条件替换它的前置条件,用更强的后置条件替换它的后置条件。如果子类前提条件强于父类前提条件,则不能在不违反前提条件的情况下用子类代替父类。因此,LSP。-客户用户2023861。

如果某些代码认为它正在调用类型T的方法,并且可能会在不知不觉中调用类型S的方法,其中S扩展了T(即,S继承、派生自超类型T或是超类型T的子类型),则LSP是必需的。

例如,在使用类型S的变量值调用(即调用)具有类型T的输入参数的函数的情况下,或者在为类型T的标识符分配类型S的值的情况下,会发生这种情况。

Val id:t=new S()//id认为它是T,但实际上是S。

LSP要求T类型的方法(例如矩形)的期望(即不变量),当调用S类型的方法(例如Square)时不会违反。

VAL RECT:Rectangle=new Square(5)//认为它是矩形,但它是正方形矩形2:Rectangle=rect.setWidth(10)//高度为10,违反LSP。

即使是具有不可变字段的类型也仍然有不变量,例如,不可变的矩形设置器希望维度被独立修改,但不可变的Square设置器违反了这一期望。

Class Rectangle(val width:int,val high:int){def setWidth(w:int)=new Rectangle(w,Height)def setHeight(h:int)=new Rectangle(width,h)}class Square(val side:int)扩展Rectangle(side,side){override def setWidth(s:int)=new Square(s:int)=new Square}

LSP要求子类型S的每个方法必须具有逆变输入参数和协变输出。

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

协方差表示方差与继承方向相同,即子类型S的每个方法的输出的类型So必须与超类型T的相应方法的相应输出的类型To相同或为其子类型。

这是因为如果调用方认为它有一个类型T,认为它正在调用一个T的方法,那么它就会提供类型Ti的参数,并将输出分配给类型To。当它实际调用S的相应方法时,则将每个Ti输入参数分配给Si输入参数,并将SO输出分配给TO类型。因此,如果Si不是逆变量,则w.r.t.。对于Ti,则可以将不是Si的子类型的Xi亚型分配给Ti。

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

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

关于如何建模不变量,以便由编译器强制执行,有很多正在进行的研究。

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

由于语义需要抽象而不是扩展,我认为使用类型来建模不变量(即统一的高阶表示语义)要优于TypeState。“扩展”指的是不协调的、模块化的开发的无界的、排列的组合。因为在我看来,有两个相互依赖的模型(例如类型和类型状态)来表达共享的语义,这两个模型对于可扩展的组合不能彼此统一,这似乎是统一性的对立面,因此也就是自由度的对立面。例如,在子类型化、函数重载和参数类型域中统一了表达式类问题扩展。

我的理论立场是,对于知识的存在(参见“集中化是盲目和不合适的”一节),永远不会有一个通用模型可以强制100%覆盖图灵完全计算机语言中所有可能的不变量。对于知识的存在,意想不到的可能性非常存在,即无序和熵必须总是在增加。这是熵力。要证明一个潜在扩张的所有可能的计算,就是先验地计算所有可能的扩张。

这就是暂停定理存在的原因,也就是说,图灵完全编程语言中的每一个可能的程序是否都是终止的,这是无法判断的。可以证明某个特定的程序终止了(所有的可能性都已经定义和计算)。但是不可能证明该程序的所有可能的扩展都终止了,除非该程序的扩展的可能性不是图灵完成的(例如,通过依赖键入)。由于图灵完备性的基本要求是无界递归,因此很容易理解哥德尔的不完备性定理和罗素悖论是如何应用于扩张的。

对这些定理的解释将它们合并到对熵力的广义概念理解中:

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

罗素悖论:可以包含一个集合的集合的每个成员规则,要么枚举每个成员的具体类型,要么包含它自己。因此,集合要么不能扩展,要么是无界递归。例如,不是茶壶的所有东西的集合包括它自己,它包括它自己,它包括它自己,等等…。。因此,如果规则(可能包含集合并且)不枚举特定类型(即允许所有未指定的类型)并且不允许无界扩展,则该规则是不一致的。这是一组不是其自身成员的集合。这种无法在所有可能的扩展上既一致又完全枚举的情况,就是哥德尔的不完全性定理。

利斯科夫置换原理:一般来说,一个集合是否为另一个集合的子集是一个不可判定的问题,即继承一般是不可判定的。

林斯基参照:当某物被描述或感知时,它的计算是什么是无法决定的,即感知(现实)没有绝对的参照点。

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

热力学第二定律:整个宇宙(封闭系统,即万物)趋向于最大无序,即最大独立可能性。

@Shelyby:你混的东西太多了。事情并不像你所说的那样令人困惑。你的许多理论断言都站不住脚,比如对于知识的存在,很多意想不到的可能性都存在,.和通常来说,一个集合是否是另一个集合的子集是一个无法决定的问题,也就是说,继承通常是无法决定的。你可以为这些观点中的每一个建立一个单独的博客。无论如何,你的断言和假设是非常值得怀疑的。一个人不能使用自己不知道的东西!--雅克农。

@aknon我有一个博客,更深入地解释了这些问题。我的无限时空模型是无限频率。递归归纳函数有已知的起始值和无穷大的终止界,或者余归纳函数有未知的终结值和已知的起始界,这对我来说并不令人困惑。一旦引入递归,相对性就成了问题。这就是图灵完成等同于无界递归的原因。-谢尔比·摩尔三世(Shelby Moore III)。

@ShelbyMooreIII你走的方向太多了。这不是一个答案。-Soldalma。

@Soldalma,这是一个答案。不要在答题部分看到它。哟。

.