我听说Liskov替换原则(LSP)是面向对象设计的基本原则.它是什么以及它的使用例子是什么?
一个很好的例子说明了LSP(我最近在一个播客中由Bob叔叔给出的)是有时在自然语言中听起来不对的东西在代码中不起作用.
在数学中,a Square
是a Rectangle
.实际上它是一个矩形的专业化."是一个"让你想要继承模型.但是,如果你在代码中Square
派生出来Rectangle
,那么a Square
应该可以在任何你期望的地方使用Rectangle
.这会产生一些奇怪的行为.
想象一下你有基类的方法SetWidth
和SetHeight
方法Rectangle
; 这似乎完全符合逻辑.但是,如果你Rectangle
参考指着一个Square
,然后SetWidth
和SetHeight
没有意义,因为设置一个将改变其他与之相匹配的.在这种情况下Square
Liskov替换测试失败Rectangle
并且Square
继承的抽象Rectangle
是一个坏的.
你们应该查看其他无价的SOLID原理励志海报.
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.
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
.
可替代性是面向对象编程中的一个原则,表明在计算机程序中,如果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{}
罗伯特马丁有一篇关于利斯科夫替代原则的优秀论文.它讨论了可能违反原则的微妙而不那么微妙的方式.
本文的一些相关部分(注意第二个例子是高度浓缩的):
一个违反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
将继承SetWidth
和SetHeight
功能.这些功能完全不适合aSquare
,因为正方形的宽度和高度是相同的.这应该是设计存在问题的重要线索.但是,有一种方法可以回避这个问题.我们可以覆盖SetWidth
和SetHeight
[...]但请考虑以下功能:
void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth }如果我们将
Square
对象的引用传递给此函数,则该Square
对象将被破坏,因为高度不会更改.这显然违反了LSP.该函数不适用于其参数的派生.[...]
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
- 它不是 - 的子类型.Si
Ti
此外,对于在类型多态性参数(即泛型)上具有定义 - 位置方差注释的语言(例如Scala或Ceylon),类型的每个类型参数的方差注释的共同或反方向T
必须相反或相同方向分别对T
具有类型参数类型的每个输入参数或输出(每个方法).
另外,对于具有功能类型的每个输入参数或输出,所需的方差方向相反.此规则以递归方式应用.
在枚举不变量的情况下,子类型是合适的.
关于如何建模不变量的研究正在进行中,因此它们由编译器强制执行.
Typestate(参见第3页)声明并强制执行与类型正交的状态不变量.或者,可以通过将断言转换为类型来强制执行不变量.例如,要在关闭文件之前声明文件已打开,则File.open()可以返回OpenFile类型,该类型包含File中不可用的close()方法.甲井字棋API可以是采用打字执行在编译时不变的另一个例子.类型系统甚至可能是图灵完备的,例如Scala.依赖类型语言和定理证明器使高阶类型的模型形式化.
由于需要语义来抽象扩展,我希望使用键入来模拟不变量,即统一的高阶指称语义,优于Typestate."扩展"是指无协调,模块化开发的无限制,置换组合.因为在我看来,它是统一的对立面,因而是自由度,有两个相互依赖的模型(例如类型和类型状态)来表达共享语义,这些模型不能相互统一以实现可扩展的组合.例如,类似表达式问题的扩展在子类型,函数重载和参数类型域中统一.
我的理论立场是,为了存在知识(参见"集中化是盲目和不合适的"一节),永远不会有一个通用模型能够在图灵完备的计算机语言中强制实现100%覆盖所有可能的不变量.为了存在知识,存在着意想不到的可能性,即无序和熵必须一直在增加.这是熵力.为了证明潜在扩展的所有可能计算,是计算先验所有可能的扩展.
这就是Halting定理存在的原因,即图灵完备编程语言中的每个可能的程序是否终止都是不可判定的.可以证明某些特定程序终止(所有可能性都已定义和计算).但是不可能证明该程序的所有可能扩展都终止,除非扩展该程序的可能性不是图灵完成(例如通过依赖类型).由于图灵完备性的基本要求是无界递归,因此直观地理解哥德尔的不完备性定理和罗素的悖论如何适用于扩展.
对这些定理的解释将它们纳入对熵力的概括性概念理解中:
哥德尔的不完备性定理:任何可以证明所有算术真理的形式理论都是不一致的.
Russell悖论:可以包含集合的集合的每个成员规则,可以枚举每个成员的特定类型或包含自身.因此集合既不能扩展也不能无限递归.例如,一套不是茶壶的东西,包括它自己,包括它自己,包括它自己等等.因此,如果规则(可能包含集合)不枚举特定类型(即允许所有未指定的类型)并且不允许无限扩展,则规则是不一致的.这是一组不属于自己的集合.哥德尔的不完备性定理是无法在所有可能的扩展中保持一致和完全列举的.
Liskov Substition Principle:通常它是一个不可判定的问题,无论任何集合是否是另一个集合的子集,即继承通常是不可判定的.
Linsky Referencing:在描述或感知事物时,某事物的计算是不可判定的,即感知(现实)没有绝对的参照点.
科斯定理:没有外部参考点,因此无限外部可能性的任何障碍都将失败.
热力学第二定律:整个宇宙(一个封闭的系统,即一切)趋向于最大程度的无序,即最大的独立可能性.
使用指针或对基类的引用的函数必须能够在不知道它的情况下使用派生类的对象.
当我第一次阅读LSP时,我认为这是非常严格意义上的,基本上将其等同于接口实现和类型安全转换.这意味着LSP要么由语言本身确保,要么得到保证.例如,在严格意义上,就编译器而言,ThreeDBoard肯定可替代Board.
在阅读了更多关于这个概念之后,我发现LSP通常比这更广泛地被解释.
简而言之,客户端代码"知道"指针后面的对象是派生类型而不是指针类型意味着什么并不限于类型安全.通过探测对象的实际行为,也可以测试对LSP的遵守情况.也就是说,检查对象的状态和方法参数对方法调用结果的影响,或者从对象抛出的异常类型.
再回到示例,理论上可以使Board方法在ThreeDBoard上正常工作.然而,在实践中,很难防止客户端可能无法正确处理的行为差异,而不会影响ThreeDBoard要添加的功能.
掌握了这些知识,评估LSP依从性可以成为确定何时组合是扩展现有功能而不是继承的更合适机制的一个很好的工具.
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是相同的.
有一个检查清单,以确定您是否违反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
使用 LSP的一个重要例子是软件测试.
如果我有一个类A是符合LSP的B类子类,那么我可以重用B的测试套件来测试A.
为了完全测试子类A,我可能需要添加一些测试用例,但至少我可以重用所有超类B的测试用例.
一种方法是通过构建McGregor所谓的"并行层次结构进行测试"来实现这一点:我的ATest
类将继承自BTest
.然后需要某种形式的注入来确保测试用例适用于类型A而不是类型B的对象(简单的模板方法模式将会这样做).
请注意,为所有子类实现重用超级测试套件实际上是一种测试这些子类实现是否符合LSP的方法.因此,人们也可以争辩说,应该在任何子类的上下文中运行超类测试套件.
另请参阅Stackoverflow问题的答案" 我可以实现一系列可重用的测试来测试接口的实现吗? "
我想每个人都在技术上涵盖了LSP:你基本上希望能够从子类型细节中抽象出来并安全地使用超类型.
所以Liskov有3个基本规则:
签名规则:语法中子类型中的超类型的每个操作都应该有效实现.编译器可以为您检查的东西.关于抛出更少的异常并且至少像超类型方法一样可访问,有一个小规则.
方法规则:这些操作的实现在语义上是合理的.
较弱的前提条件:子类型函数应至少采用超类型作为输入的内容,如果不是更多.
更强的后置条件:它们应该生成超类型方法产生的输出的子集.
属性规则:这超出了单个函数调用.
不变量:始终如一的事物必须保持真实.例如.Set的大小永远不会消极.
进化属性:通常与不变性或对象可以处于的状态有关.或者对象只能增长而不会缩小,因此子类型方法不应该成功.
需要保留所有这些属性,并且额外的子类型功能不应违反超类型属性.
如果这三件事都得到了解决,那么你已经从基础内容中抽象出来了,并且你正在编写松散耦合的代码.
资料来源:Java项目开发 - Barbara Liskov
我在每个答案中看到矩形和正方形,以及如何违反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..:
在一个非常简单的句子中,我们可以说:
子类不得违反其基类特征.它必须有能力.我们可以说它与子类型相同.