Not*_*elf 845 oop liskov-substitution-principle definition design-principles solid-principles
我听说Liskov替换原则(LSP)是面向对象设计的基本原则.它是什么以及它的使用例子是什么?
m-s*_*arp 813
一个很好的例子说明了LSP(我最近在一个播客中由Bob叔叔给出的)是有时在自然语言中听起来不对的东西在代码中不起作用.
在数学中,a Square是a Rectangle.实际上它是一个矩形的专业化."是一个"让你想要继承模型.但是,如果你在代码中Square派生出来Rectangle,那么a Square应该可以在任何你期望的地方使用Rectangle.这会产生一些奇怪的行为.
想象一下你有基类的方法SetWidth和SetHeight方法Rectangle; 这似乎完全符合逻辑.但是,如果你Rectangle参考指着一个Square,然后SetWidth和SetHeight没有意义,因为设置一个将改变其他与之相匹配的.在这种情况下SquareLiskov替换测试失败Rectangle并且Square继承的抽象Rectangle是一个坏的.

你们应该查看其他无价的SOLID原理励志海报.
Not*_*elf 464
Liskov替换原则(LSP,lsp)是面向对象编程中的一个概念,它指出:
使用指针或对基类的引用的函数必须能够在不知道它的情况下使用派生类的对象.
从本质上讲,LSP是关于接口和契约以及如何决定何时扩展一个类而不是使用另一个策略(如组合)来实现您的目标.
我所看到的最有效的方式就是Head First OOA&D.他们提出了一个场景,您是一个项目开发人员,为战略游戏构建框架.
他们提出了一个代表董事会的类,如下所示:
![]()
所有方法都将X和Y坐标作为参数来定位二维数组中的tile位置Tiles.这将允许游戏开发者在游戏过程中管理棋盘中的单元.
这本书继续改变要求,说游戏框架工作也必须支持3D游戏板,以适应有飞行的游戏.因此ThreeDBoard引入了一个扩展的类Board.
乍一看,这似乎是一个很好的决定.Board提供Height和Width属性并ThreeDBoard提供Z轴.
它崩溃的地方是你看看继承自的所有其他成员Board.对于这些方法AddUnit,GetTile,GetUnits等等,都需要在X和Y参数Board类,但ThreeDBoard需要一个参数Z为好.
因此,您必须使用Z参数再次实现这些方法.Z参数没有Board类的上下文,类中继承的方法Board失去了意义.试图使用ThreeDBoard该类作为其基类的代码单元Board将非常不幸.
也许我们应该找到另一种方法.而不是扩展Board,ThreeDBoard应该由Board对象组成.Board每单位Z轴一个对象.
这允许我们使用良好的面向对象原则,如封装和重用,并且不违反LSP.
Kon*_*lph 127
LSP涉及不变量.
下面的伪代码声明给出了经典示例(省略了实现):
class Rectangle {
int getHeight()
void setHeight(int value)
int getWidth()
void setWidth(int value)
}
class Square : Rectangle { }
Run Code Online (Sandbox Code Playgroud)
现在我们遇到了一个问题,尽管界面匹配.原因是我们违反了由正方形和矩形的数学定义产生的不变量.getter和setter的工作方式,a Rectangle应满足以下不变量:
void invariant(Rectangle r) {
r.setHeight(200)
r.setWidth(100)
assert(r.getHeight() == 200 and r.getWidth() == 100)
}
Run Code Online (Sandbox Code Playgroud)
但是,必须通过正确的实现来违反此不变量Square,因此它不是有效的替代Rectangle.
May*_*ndi 117
可替代性是面向对象编程中的一个原则,表明在计算机程序中,如果S是T的子类型,则类型T的对象可以用类型S的对象替换
让我们用Java做一个简单的例子:
public class Bird{
public void fly(){}
}
public class Duck extends Bird{}
Run Code Online (Sandbox Code Playgroud)
鸭子可以飞,因为它是一只鸟,但是这个怎么样:
public class Ostrich extends Bird{}
Run Code Online (Sandbox Code Playgroud)
鸵鸟是一只鸟,但它不能飞,鸵鸟类是Bird类的子类,但它不能使用fly方法,这意味着我们正在打破LSP原理.
public class Bird{
}
public class FlyingBirds extends Bird{
public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}
Run Code Online (Sandbox Code Playgroud)
Phi*_*lls 74
罗伯特马丁有一篇关于利斯科夫替代原则的优秀论文.它讨论了可能违反原则的微妙而不那么微妙的方式.
本文的一些相关部分(注意第二个例子是高度浓缩的):
一个违反LSP的简单例子
最明显违反此原则的行为之一是使用C++运行时类型信息(RTTI)根据对象的类型选择函数.即:
Run Code Online (Sandbox Code Playgroud)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类的每个可能的衍生物,并且必须在Shape创建新的衍生物时进行更改.实际上,许多人认为这个函数的结构是面向对象设计的诅咒.正方形和矩形,更微妙的违规.
然而,还有其他更微妙的违反LSP的方式.考虑使用如下所述
Rectangle类的应用程序:Run Code Online (Sandbox Code Playgroud)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将继承SetWidth和SetHeight功能.这些功能完全不适合aSquare,因为正方形的宽度和高度是相同的.这应该是设计存在问题的重要线索.但是,有一种方法可以回避这个问题.我们可以覆盖SetWidth和SetHeight[...]但请考虑以下功能:
Run Code Online (Sandbox Code Playgroud)void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth }如果我们将
Square对象的引用传递给此函数,则该Square对象将被破坏,因为高度不会更改.这显然违反了LSP.该函数不适用于其参数的派生.[...]
She*_*III 40
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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
即使是具有不可变字段的类型仍然具有不变量,例如,不可变的 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)
}
Run Code Online (Sandbox Code Playgroud)
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定理存在的原因,即图灵完备编程语言中的每个可能的程序是否终止都是不可判定的.可以证明某些特定程序终止(所有可能性都已定义和计算).但是不可能证明该程序的所有可能扩展都终止,除非扩展该程序的可能性不是图灵完成(例如通过依赖类型).由于图灵完备性的基本要求是无界递归,因此直观地理解哥德尔的不完备性定理和罗素的悖论如何适用于扩展.
对这些定理的解释将它们纳入对熵力的概括性概念理解中:
Chr*_*man 20
使用指针或对基类的引用的函数必须能够在不知道它的情况下使用派生类的对象.
当我第一次阅读LSP时,我认为这是非常严格意义上的,基本上将其等同于接口实现和类型安全转换.这意味着LSP要么由语言本身确保,要么得到保证.例如,在严格意义上,就编译器而言,ThreeDBoard肯定可替代Board.
在阅读了更多关于这个概念之后,我发现LSP通常比这更广泛地被解释.
简而言之,客户端代码"知道"指针后面的对象是派生类型而不是指针类型意味着什么并不限于类型安全.通过探测对象的实际行为,也可以测试对LSP的遵守情况.也就是说,检查对象的状态和方法参数对方法调用结果的影响,或者从对象抛出的异常类型.
再回到示例,理论上可以使Board方法在ThreeDBoard上正常工作.然而,在实践中,很难防止客户端可能无法正确处理的行为差异,而不会影响ThreeDBoard要添加的功能.
掌握了这些知识,评估LSP依从性可以成为确定何时组合是扩展现有功能而不是继承的更合适机制的一个很好的工具.
Cha*_*tin 20
LSP是关于clases合同的规则:如果基类满足合同,那么LSP派生类也必须满足该合同.
在Pseudo-python中
class Base:
def Foo(self, arg):
# *... do stuff*
class Derived(Base):
def Foo(self, arg):
# *... do stuff*
Run Code Online (Sandbox Code Playgroud)
如果每次在Derived对象上调用Foo时,它都会满足LSP,它给出的结果与在Base对象上调用Foo完全相同,只要arg是相同的.
Cù *_*iếu 19
有一个检查清单,以确定您是否违反Liskov.
清单:
历史约束:覆盖方法时,不允许修改基类中的不可修改属性.看看这些代码,您可以看到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);
}
}
Run Code Online (Sandbox Code Playgroud)还有2个项目:方法参数的反演和返回类型的协方差.但是在C#中我不可能(我是C#开发人员)所以我不关心他们.
参考:
ava*_*sen 18
使用 LSP的一个重要例子是软件测试.
如果我有一个类A是符合LSP的B类子类,那么我可以重用B的测试套件来测试A.
为了完全测试子类A,我可能需要添加一些测试用例,但至少我可以重用所有超类B的测试用例.
一种方法是通过构建McGregor所谓的"并行层次结构进行测试"来实现这一点:我的ATest类将继承自BTest.然后需要某种形式的注入来确保测试用例适用于类型A而不是类型B的对象(简单的模板方法模式将会这样做).
请注意,为所有子类实现重用超级测试套件实际上是一种测试这些子类实现是否符合LSP的方法.因此,人们也可以争辩说,应该在任何子类的上下文中运行超类测试套件.
另请参阅Stackoverflow问题的答案" 我可以实现一系列可重用的测试来测试接口的实现吗? "
sna*_*aul 17
我想每个人都在技术上涵盖了LSP:你基本上希望能够从子类型细节中抽象出来并安全地使用超类型.
所以Liskov有3个基本规则:
签名规则:语法中子类型中的超类型的每个操作都应该有效实现.编译器可以为您检查的东西.关于抛出更少的异常并且至少像超类型方法一样可访问,有一个小规则.
方法规则:这些操作的实现在语义上是合理的.
属性规则:这超出了单个函数调用.
需要保留所有这些属性,并且额外的子类型功能不应违反超类型属性.
如果这三件事都得到了解决,那么你已经从基础内容中抽象出来了,并且你正在编写松散耦合的代码.
资料来源:Java项目开发 - Barbara Liskov
Ste*_*ard 15
我在每个答案中看到矩形和正方形,以及如何违反LSP.
我想展示如何通过一个真实世界的例子来符合LSP:
<?php
interface Database
{
public function selectQuery(string $sql): array;
}
class SQLiteDatabase implements Database
{
public function selectQuery(string $sql): array
{
// sqlite specific code
return $result;
}
}
class MySQLDatabase implements Database
{
public function selectQuery(string $sql): array
{
// mysql specific code
return $result;
}
}
Run Code Online (Sandbox Code Playgroud)
此设计符合LSP,因为无论我们选择使用哪种实现,行为都保持不变.
是的,您可以在此配置中违反LSP,执行一个简单的更改,如下所示:
<?php
interface Database
{
public function selectQuery(string $sql): array;
}
class SQLiteDatabase implements Database
{
public function selectQuery(string $sql): array
{
// sqlite specific code
return $result;
}
}
class MySQLDatabase implements Database
{
public function selectQuery(string $sql): array
{
// mysql specific code
return ['result' => $result]; // This violates LSP !
}
}
Run Code Online (Sandbox Code Playgroud)
现在,子类型不能以相同的方式使用,因为它们不再产生相同的结果.
Blo*_*kas 14
龙的故事,总之,让延伸的父类,当我们离开矩形长方形和正方形广场,实际的例子,你必须要么保留确切父API或把它扩大.
假设您有一个基于 ItemsRepository 的基础.
class ItemsRepository
{
/**
* @return int Returns number of deleted rows
*/
public function delete()
{
// perform a delete query
$numberOfDeletedRows = 10;
return $numberOfDeletedRows;
}
}
Run Code Online (Sandbox Code Playgroud)
还有一个扩展它的子类:
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;
}
}
Run Code Online (Sandbox Code Playgroud)
然后,您可以让客户端使用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();
}
}
Run Code Online (Sandbox Code Playgroud)
当用子类替换父类时,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 :(
}
}
Run Code Online (Sandbox Code Playgroud)
您可以在我的课程中了解有关编写可维护软件的更多信息:https://www.udemy.com/enterprise-php/
LSP的这种表述过于强烈:
如果对于类型S的每个对象o1,存在类型为T的对象o2,使得对于根据T定义的所有程序P,当o1代替o2时P的行为不变,则S是T的子类型.
这基本上意味着S是另一个完全封装的与T完全相同的实现.我可以大胆并决定性能是P的行为的一部分......
所以,基本上,任何后期绑定的使用都违反了LSP.当我们将一种对象替换为另一种对象时,获得不同的行为是OO的重点!
维基百科引用的公式更好,因为该属性取决于上下文,并不一定包括该程序的整个行为.
该原则由Barbara Liskov于 1987年引入,并通过关注超类及其子类型的行为扩展了开闭原则。
当我们考虑违反它的后果时,它的重要性就变得显而易见了。考虑一个使用以下类的应用程序。
public class Rectangle
{
private double width;
private double height;
public double Width
{
get
{
return width;
}
set
{
width = value;
}
}
public double Height
{
get
{
return height;
}
set
{
height = value;
}
}
}
Run Code Online (Sandbox Code Playgroud)
想象一下,有一天,客户要求除了处理矩形之外还能够操作正方形。由于正方形是矩形,因此正方形类应从 Rectangle 类派生。
public class Square : Rectangle
{
}
Run Code Online (Sandbox Code Playgroud)
但是,这样做我们会遇到两个问题:
正方形不需要从矩形继承的高度和宽度变量,如果我们必须创建数十万个正方形对象,这可能会造成显着的内存浪费。从矩形继承的宽度和高度设置器属性不适用于正方形,因为正方形的宽度和高度是相同的。为了将高度和宽度设置为相同的值,我们可以创建两个新属性,如下所示:
public class Square : Rectangle
{
public double SetWidth
{
set
{
base.Width = value;
base.Height = value;
}
}
public double SetHeight
{
set
{
base.Height = value;
base.Width = value;
}
}
}
Run Code Online (Sandbox Code Playgroud)
现在,当有人设置正方形对象的宽度时,它的高度会相应地改变,反之亦然。
Square s = new Square();
s.SetWidth(1); // Sets width and height to 1.
s.SetHeight(2); // sets width and height to 2.
Run Code Online (Sandbox Code Playgroud)
让我们继续前进并考虑另一个函数:
public void A(Rectangle r)
{
r.SetWidth(32); // calls Rectangle.SetWidth
}
Run Code Online (Sandbox Code Playgroud)
如果我们将一个方形对象的引用传递给这个函数,我们将违反 LSP,因为该函数不适用于其参数的派生。属性宽度和高度不是多态的,因为它们没有在矩形中声明为虚拟(正方形对象将被破坏,因为高度不会改变)。
但是,通过将 setter 属性声明为虚拟,我们将面临另一个违规行为,即 OCP。事实上,派生类正方形的创建会导致基类矩形发生变化。
小智 8
里斯科夫的替代原理(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.
}
}
Run Code Online (Sandbox Code Playgroud)
结论:
该原理只是“打开关闭”原理的扩展,它意味着我们必须确保新的派生类在不改变其行为的情况下扩展了基类。
另请参阅:开闭原理
一些类似的概念可以改善结构:约定优于配置
让我们用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() { ... }
}
Run Code Online (Sandbox Code Playgroud)
这里没问题吧?汽车绝对是一种运输工具,在这里我们可以看到它覆盖了其超类的startEngine()方法。
让我们添加另一个运输设备:
class Bicycle extends TransportationDevice
{
@Override
void startEngine() /*problem!*/
}
Run Code Online (Sandbox Code Playgroud)
现在一切都没有按计划进行!是的,自行车是一种运输设备,但是它没有引擎,因此无法实现startEngine()方法。
这些都是违反Liskov替代原理而导致的问题,通常可以通过什么都不做甚至无法实现的方法来识别。
这些问题的解决方案是正确的继承层次结构,在我们的案例中,我们将通过区分带有和不带有引擎的运输设备的类别来解决该问题。即使自行车是运输工具,它也没有引擎。在此示例中,我们对运输设备的定义是错误的。它不应该有引擎。
我们可以如下重构我们的TransportationDevice类:
class TrasportationDevice
{
String name;
String getName() { ... }
void setName(String n) { ... }
double speed;
double getSpeed() { ... }
void setSpeed(double d) { ... }
}
Run Code Online (Sandbox Code Playgroud)
现在,我们可以将TransportationDevice扩展为非机动设备。
class DevicesWithoutEngines extends TransportationDevice
{
void startMoving() { ... }
}
Run Code Online (Sandbox Code Playgroud)
并将TransportationDevice扩展为电动设备。在这里添加引擎对象更为合适。
class DevicesWithEngines extends TransportationDevice
{
Engine engine;
Engine getEngine() { ... }
void setEngine(Engine e) { ... }
void startEngine() { ... }
}
Run Code Online (Sandbox Code Playgroud)
因此,我们的汽车课在遵循Liskov替代原则的同时变得更加专业。
class Car extends DevicesWithEngines
{
@Override
void startEngine() { ... }
}
Run Code Online (Sandbox Code Playgroud)
我们的自行车课也符合《里斯科夫换人原则》。
class Bicycle extends DevicesWithoutEngines
{
@Override
void startMoving() { ... }
}
Run Code Online (Sandbox Code Playgroud)
小智 8
它指出,如果 C 是 E 的子类型,则可以用 C 类型的对象替换 E,而不会更改或破坏程序的行为。简而言之,派生类应该可以替换其父类。例如,如果Farmer\xe2\x80\x99s 的儿子是农民,那么他可以代替他父亲工作,但如果Farmer\xe2\x80\x99s 的儿子是板球运动员,那么他可以\xe2\x80\x99t 在原地工作他父亲的。
\npublic class Plane{\n\n public void startEngine(){} \n\n} \npublic class FighterJet extends Plane{}\n \npublic class PaperPlane extends Plane{}\nRun Code Online (Sandbox Code Playgroud)\n在给定的示例中FighterPlane,PaperPlane类都扩展了Plane包含startEngine()方法的类。所以很明显FighterPlane可以启动引擎但PaperPlane可以\xe2\x80\x99t所以它\xe2\x80\x99s打破了LSP。
PaperPlane类虽然扩展了Plane类并且应该可以替换它,但不是 Plane\xe2\x80\x99s 实例可以替换的合格实体,因为纸飞机不能 \xe2\x80\x99s 启动引擎,因为它不\ xe2\x80\x99t 有一个。所以一个好的例子是,
public class Plane{ \n} \npublic class RealPlane{\n\n public void startEngine(){} \n\n}\npublic class FighterJet extends RealPlane{} \npublic class PaperPlane extends Plane{}\nRun Code Online (Sandbox Code Playgroud)\n
一些附录:
我想知道为什么没有人写关于派生类必须遵守的基类的不变量,前置条件和后置条件.要使派生类D完全可由Base类B维护,D类必须遵守某些条件:
因此派生必须知道基类强加的上述三个条件.因此,子类型的规则是预先确定的.这意味着,只有当子类型遵守某些规则时,才应遵守"IS A"关系.这些规则以不变量,预先准备和后置条件的形式,应由正式的" 设计合同 " 决定.
关于这方面的进一步讨论可以在我的博客上找到:Liskov Substitution原则
大局:
与其他答案不同,我不会从违反里氏替换原则 (LSP) 开始,而是从遵守 LSP 开始。我使用 Java,但在每种 OOP 语言中几乎都是一样的。
Circle和ColoredCircle几何例子在这里似乎很受欢迎。
class Circle {
private int radius;
public Circle(int radius) {
if (radius < 0) {
throw new RuntimeException("Radius should be >= 0");
}
this.radius = radius;
}
public int getRadius() {
return this.radius;
}
}
Run Code Online (Sandbox Code Playgroud)
半径不允许为负数。这是一个子类:
class ColoredCircle extends Circle {
private Color color; // defined elsewhere
public ColoredCircle(int radius, Color color) {
super(radius);
this.color = color;
}
public Color getColor() {
return this.color;
}
}
Run Code Online (Sandbox Code Playgroud)
Circle根据 LSP,该子类是 的子类型。
LSP 指出:
如果对于 S 类型的每个对象 o1 都有一个 T 类型的对象 o2,使得对于用 T 定义的所有程序 P,当 o1 替换 o2 时 P 的行为保持不变,则 S 是 T 的子类型。( Barbara Liskov,“数据抽象和层次结构”,SIGPLAN 公告,23,5(1988 年 5 月)
这里,对于每个ColoredCircle实例o1,考虑Circle具有相同半径的实例o2。对于每个使用Circle对象的程序,如果替换o2为o1,则任何使用对象的程序的行为Circle在替换后将保持不变。ColoredCircle(请注意,这是理论上的:使用实例会比使用实例更快地耗尽内存Circle,但这与此无关。)
我们如何找到o2依赖项o1?我们只是剥离color属性并保留radius属性。我将这种变换称为o1从空间到空间的o2投影。CircleColorCircle
让我们创建另一个例子来说明 LSP 的违规情况。
Circle和Square想象一下前一个类的子类Circle:
class Square extends Circle {
private int sideSize;
public Square(int sideSize) {
super(0);
this.sideSize = sideSize;
}
@Override
public int getRadius() {
return -1; // I'm a square, I don't care
}
public int getSideSize() {
return this.sideSize;
}
}
Run Code Online (Sandbox Code Playgroud)
现在,看看这个程序:
public class Liskov {
public static void program(Circle c) {
System.out.println("The radius is "+c.getRadius());
}
Run Code Online (Sandbox Code Playgroud)
Circle我们用一个对象和一个对象来测试程序Square。
public static void main(String [] args){
Liskov.program(new Circle(2)); // prints "The radius is 2"
Liskov.program(new Square(2)); // prints "The radius is -1"
}
}
Run Code Online (Sandbox Code Playgroud)
发生了什么 ?直观上,虽然Square是 的子类Circle,但不是Square的子类型,因为没有任何常规实例的半径为 -1。CircleCircle
从形式上来说,这违反了里氏替换原则。
我们有一个根据 定义的程序,Circle并且在该程序中没有任何Circle对象可以替换new Square(2)(或任何Square实例)并且保持行为不变:记住任何半径Circle始终为正。
现在我们知道为什么子类并不总是subtype。当子类不是子类型时,即当存在 LSP 违规时,某些程序(至少一个)的行为并不总是预期的行为。这是非常令人沮丧的,通常被解释为一个错误。
在理想的世界中,编译器或解释器将能够检查给定的子类是否是真正的子类型,但我们并不处于理想的世界中。
如果有一些静态类型,您将在编译时受到超类签名的约束。Square.getRadius()无法返回 aString或 a List。
如果没有静态类型,如果一个参数的类型错误(除非类型很弱)或参数数量不一致(除非语言非常宽松),那么您将在运行时收到错误。
关于静态类型的注意事项:存在返回类型的协变机制(S 的方法可以返回 T 的同一方法的返回类型的子类)和参数类型的逆变机制(S 的方法可以接受T) 的相同方法的相同参数的参数的超类。这是下面解释的前置条件和后置条件的具体情况。
还有更多。有些语言(我认为是 Eiffel)提供了一种强制遵守 LSP 的机制。
更不用说确定o2初始对象的投影了o1,我们可以预期任何程序的相同行为 ifo1替换o2if,对于任何参数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)。(请注意,如果函数是纯函数,则(3)是免费给出的f。这就是为什么我们喜欢拥有不可变对象。)
这些条件与类的语义(期望的内容)有关,而不仅仅是类的语法。而且,这些条件都非常强大。但它们可以通过合约编程设计中的断言来近似。这些断言是确保维护类型语义的一种方法。违反契约会导致运行时错误。
S.f接受超过T.f)(a)。S.f提供超过T.f)(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 得到支持的宝贵工具。
正方形是宽度等于高度的矩形。如果正方形为宽度和高度设置了两种不同的大小,则违反了正方形不变量。这是通过引入副作用来解决的。但是如果矩形有一个 setSize(height, width) ,前提是 0 < 高度和 0 < 宽度。派生子类型方法需要高度==宽度;更强的先决条件(并且违反了 lsp)。这表明虽然 square 是一个矩形,但它不是有效的子类型,因为前提条件得到了加强。解决方法(通常是坏事)会导致副作用,这会削弱后期条件(违反 lsp)。底座上的 setWidth 具有后置条件 0 < 宽度。派生的高度 == 宽度削弱了它。
因此,可调整大小的正方形不是可调整大小的矩形。
| 归档时间: |
|
| 查看次数: |
291292 次 |
| 最近记录: |