我听说Liskov替换原则(LSP)是面向对象设计的基本原则。它是什么以及它的使用例子是什么?
一个很好的例子说明了LSP(我最近在一个播客中由Bob叔叔给出的)是有时在自然语言中听起来不对的东西在代码中不起作用。
在数学中,Square
是Rectangle
。实际上它是一个矩形的专业化。 “是一个”让你想要继承模型。但是,如果在代码中你使Square
来自Rectangle
,那么Square
应该可用于任何你期望的Rectangle
。这会产生一些奇怪的行为。
想象一下,你的SetWidth
基类上有SetHeight
和Rectangle
方法;这似乎完全符合逻辑。然而,如果你的Rectangle
参考指向Square
,那么SetWidth
和SetHeight
没有意义,因为设置一个会改变另一个匹配它。在这种情况下,Square
未能通过Rectangle
进行Liskov替换测试,并且从Square
继承Rectangle
的抽象是不好的。
你们应该看看其他无价的SOLID Principles Motivational Posters。
使用LSP的一个重要例子是软件测试。
如果我有一个类A是符合LSP的B类子类,那么我可以重用B的测试套件来测试A.
为了完全测试子类A,我可能需要添加一些测试用例,但至少我可以重用所有超类B的测试用例。
一种实现的方法是通过构建McGregor所谓的“并行层次结构进行测试”:我的ATest
类将继承自BTest
。然后需要某种形式的注入来确保测试用例适用于类型A而不是类型B的对象(简单的模板方法模式将会这样做)。
请注意,为所有子类实现重用超级测试套件实际上是一种测试这些子类实现是否符合LSP的方法。因此,人们也可以争辩说,应该在任何子类的上下文中运行超类测试套件。
另请参阅Stackoverflow问题“Can I implement a series of reusable tests to test an interface's implementation?”的答案
我想每个人都在技术上涵盖了LSP:你基本上希望能够从子类型细节中抽象出来并安全地使用超类型。
所以Liskov有3个基本规则:
需要保留所有这些属性,并且额外的子类型功能不应违反超类型属性。
如果这三件事都得到了解决,那么你已经从基础内容中抽象出来了,并且你正在编写松散耦合的代码。
资料来源:Java项目开发 - Barbara Liskov
长话短说,让我们留下矩形矩形和正方形方块,扩展父类时的实际示例,您必须保留确切的父API或EXTEND IT。
假设您有一个基于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/
LSP的这种表述过于强烈:
如果对于类型S的每个对象o1,存在类型为T的对象o2,使得对于根据T定义的所有程序P,当o1代替o2时P的行为不变,则S是T的子类型。
这基本上意味着S是另一个完全封装的与T完全相同的实现。我可以大胆并决定性能是P的行为的一部分......
所以,基本上,任何后期绑定的使用都违反了LSP。当我们将一种对象替换为另一种对象时,获得不同的行为是OO的重点!
引用by wikipedia的表述更好,因为该属性取决于上下文,并不一定包括该程序的整个行为。
我在每个答案中看到矩形和正方形,以及如何违反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;
}
}
此设计符合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 !
}
}
现在,子类型不能以相同的方式使用,因为它们不再产生相同的结果。
一些附录: 我想知道为什么没有人写关于派生类必须遵守的基类的不变量,前置条件和后置条件。要使派生类D完全可由Base类B维护,D类必须遵守某些条件:
因此派生必须知道基类强加的上述三个条件。因此,子类型的规则是预先确定的。这意味着,只有当子类型遵守某些规则时,才应遵守“IS A”关系。这些规则,以不变量,预先准备和后置条件的形式,应由正式的'design contract'决定。
关于这方面的进一步讨论可以在我的博客上找到:Liskov Substitution principle
在一个非常简单的句子中,我们可以说:
子类不得违反其基类特征。它必须有能力。我们可以说它与子类型相同。
Liscover Substitution Principal(LPS)
我们一直在设计程序模块,并创建了一些类层次结构。然后我们扩展一些类创建一些派生类。
我们必须确保新的派生类只是扩展而不替换旧类的功能。否则,新类在现有程序模块中使用时会产生不良影响。
Liskov的替换原则指出,如果程序模块使用Base类,则可以使用Derived类替换对Base类的引用,而不会影响程序模块的功能。
例:
以下是违反Liskov替代原则的典型例子。在该示例中,使用了2个类:Rectangle和Square。我们假设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.
}
}
结论:
这个原则只是Open Close Principle的扩展,它意味着我们必须确保新派生类扩展基类而不改变它们的行为。
更好结构的一些类似概念:Convention over configuration
在一系列董事会中实施ThreeDBoard会有用吗?
也许您可能希望将各种平面中的ThreeDBoard切片视为Board。在这种情况下,您可能希望抽象出Board的接口(或抽象类)以允许多个实现。
在外部接口方面,您可能想要为TwoDBoard和ThreeDBoard分配Board接口(尽管上述方法都不合适)。
正方形是一个矩形,其宽度等于高度。如果正方形为宽度和高度设置两个不同的大小,则它违反了方形不变量。这通过引入副作用来解决。但是如果矩形有一个setSize(高度,宽度),前提条件为0 <height和0 <width。派生的子类型方法需要height == width;一个更强的先决条件(并且违反了lsp)。这表明虽然square是一个矩形,但它不是一个有效的子类型,因为前提条件得到了加强。解决方法(通常是坏事)会导致副作用,这会削弱后置条件(违反lsp)。基础上的setWidth具有post条件0 <width。派生用height == width来削弱它。
因此,可调整大小的正方形不是可调整大小的矩形。
Liskov替换原则(LSP,lsp)是面向对象编程中的一个概念,它指出:
使用指针或对基类的引用的函数必须能够在不知道它的情况下使用派生类的对象。
从本质上讲,LSP是关于接口和契约以及如何决定何时扩展一个类而不是使用另一个策略(如组合)来实现您的目标。
我所看到的最有效的方式来说明这一点是在Head First OOA&D。他们提出了一个场景,您是一个项目开发人员,为战略游戏构建框架。
他们提出了一个代表董事会的类,如下所示:
所有方法都将X和Y坐标作为参数,以在Tiles
的二维数组中定位切片位置。这将允许游戏开发者在游戏过程中管理棋盘中的单元。
这本书继续改变要求,说游戏框架工作也必须支持3D游戏板,以适应有飞行的游戏。因此引入了扩展ThreeDBoard
的Board
类。
乍一看,这似乎是一个很好的决定。 Board
提供Height
和Width
属性,ThreeDBoard
提供Z轴。
当你看到从Board
继承的所有其他成员时,它崩溃的地方。 AddUnit
,GetTile
,GetUnits
等的方法都采用Board
类中的X和Y参数,但ThreeDBoard
也需要Z参数。
因此,您必须使用Z参数再次实现这些方法。 Z参数没有Board
类的上下文,Board
类的继承方法失去了意义。尝试使用ThreeDBoard
类作为基类Board
的代码单元将非常不幸。
也许我们应该找到另一种方法。 Board
应该由ThreeDBoard
对象组成,而不是扩展Board
。每单位Z轴一个Board
对象。
这允许我们使用良好的面向对象原则,如封装和重用,并且不违反LSP。
让我们用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用于电动设备。这里更适合添加Engine对象。
class DevicesWithEngines extends TransportationDevice
{
Engine engine;
Engine getEngine() { ... }
void setEngine(Engine e) { ... }
void startEngine() { ... }
}
因此,我们的Car类更加专业化,同时遵守Liskov替换原则。
class Car extends DevicesWithEngines
{
@Override
void startEngine() { ... }
}
我们的自行车课程也符合Liskov替代原则。
class Bicycle extends DevicesWithoutEngines
{
@Override
void startMoving() { ... }
}
假设我们在代码中使用矩形
r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);
在我们的几何类中,我们了解到一个正方形是一种特殊类型的矩形,因为它的宽度与它的高度相同。让我们根据这些信息制作一个Square
课程:
class Square extends Rectangle {
setDimensions(width, height){
assert(width == height);
super.setDimensions(width, height);
}
}
如果我们在第一个代码中用Rectangle
替换Square
,那么它将会破坏:
r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);
这是因为Square
有一个我们在Rectangle
类中没有的新前提条件:width == height
。根据LSP,Rectangle
实例应该可以用Rectangle
子类实例替代。这是因为这些实例传递了Rectangle
实例的类型检查,因此它们会在代码中导致意外错误。
这是wiki article中“亚型”中的“先决条件无法加强”部分的一个例子。总而言之,违反LSP可能会在某些时候导致代码错误。
我鼓励你阅读这篇文章:Violating Liskov Substitution Principle (LSP)。
你可以在那里找到解释什么是Liskov替换原则,一般线索可以帮助你猜测你是否已经违反它,以及一个帮助你使你的类层次更安全的方法示例。
到目前为止我发现的LSP的最清楚的解释是“Liskov替换原则说明派生类的对象应该能够替换基类的对象,而不会在系统中引入任何错误或修改基类的行为“来自here。本文给出了违反LSP并修复它的代码示例。
LISKOV替换原则(来自Mark Seemann一书)指出,我们应该能够在不破坏客户端或实现的情况下将接口的一个实现替换为另一个。即使我们可以',这个原则也能够满足将来发生的需求。今天预见到他们。
如果我们从墙上拔掉电脑(实施),墙上插座(接口)和电脑(客户端)都不会发生故障(实际上,如果是笔记本电脑,它甚至可以在电池上运行一段时间) 。但是,对于软件,客户通常希望服务可用。如果删除了服务,我们会得到NullReferenceException。为了处理这种情况,我们可以创建一个“什么也没做”的接口实现。这是一种称为Null Object的设计模式,[4]它大致相当于将计算机从墙上拔下来。因为我们正在使用松散耦合,所以我们可以用一些不会造成麻烦的东西替换真正的实现。
Likov的替换原则指出,如果程序模块使用Base类,则可以用Derived类替换对Base类的引用,而不会影响程序模块的功能。
意图 - 派生类型必须完全替代其基本类型。
示例 - java中的Co-variant返回类型。
LSP说''对象应该可以被它们的子类替换''。另一方面,这个原则指向
子类永远不应该破坏父类的类型定义。
以下示例有助于更好地理解LSP。
没有LSP:
public interface CustomerLayout{
public void render();
}
public FreeCustomer implements CustomerLayout {
...
@Override
public void render(){
//code
}
}
public PremiumCustomer implements CustomerLayout{
...
@Override
public void render(){
if(!hasSeenAd)
return; //it isn`t rendered in this case
//code
}
}
public void renderView(CustomerLayout layout){
layout.render();
}
通过LSP修复:
public interface CustomerLayout{
public void render();
}
public FreeCustomer implements CustomerLayout {
...
@Override
public void render(){
//code
}
}
public PremiumCustomer implements CustomerLayout{
...
@Override
public void render(){
if(!hasSeenAd)
showAd();//it has a specific behavior based on its requirement
//code
}
}
public void renderView(CustomerLayout layout){
layout.render();
}
让我试一下,考虑一个界面:
interface Planet{
}
这是通过类实现的:
class Earth implements Planet {
public $radius;
public function construct($radius) {
$this->radius = $radius;
}
}
您将使用地球:
$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();
现在考虑另外一个扩展地球的类:
class LiveablePlanet extends Earth{
public function color(){
}
}
现在根据LSP,您应该能够使用LiveablePlanet代替地球,它不应该破坏您的系统。喜欢:
$planet = new LiveablePlanet(6371); // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();
从here采取的例子
以下是this post的摘录,可以很好地澄清事情:
[...]为了理解某些原则,重要的是要意识到它何时被违反。这就是我现在要做的。
违反这一原则意味着什么?它意味着一个对象不能满足用接口表示的抽象强加的契约。换句话说,这意味着您确定了您的抽象错误。
请考虑以下示例:
interface Account
{
/**
* Withdraw $money amount from this account.
*
* @param Money $money
* @return mixed
*/
public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
private $balance;
public function withdraw(Money $money)
{
if (!$this->enoughMoney($money)) {
return;
}
$this->balance->subtract($money);
}
}
这是违反LSP的吗?是。这是因为该帐户的合同告诉我们帐户将被撤销,但情况并非总是如此。那么,我该怎么做才能解决它?我只是修改合同:
interface Account
{
/**
* Withdraw $money amount from this account if its balance is enough.
* Otherwise do nothing.
*
* @param Money $money
* @return mixed
*/
public function withdraw(Money $money);
}
Voilà,现在合同很满意。
这种微妙的违规通常会使客户能够分辨出所使用的具体对象之间的区别。例如,根据第一个帐户的合同,它可能如下所示:
class Client
{
public function go(Account $account, Money $money)
{
if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
return;
}
$account->withdraw($money);
}
}
并且,这自动违反了开放式原则[即,对于提款要求。因为如果违反合同的对象没有足够的钱,你永远不会知道会发生什么。可能它只返回任何内容,可能会抛出异常。所以你必须检查它是否hasEnoughMoney()
- 这不是一个接口的一部分。所以这个强制的具体类依赖检查是OCP违规]。
这一点也解决了我经常遇到的关于LSP违规的错误观念。它说“如果父母的行为在孩子身上发生了变化,那么就会违反LSP。”然而,只要孩子没有违反其父母的合同,它就不会。
LSP涉及不变量。
下面的伪代码声明给出了经典示例(省略了实现):
class Rectangle {
int getHeight()
void setHeight(int value)
int getWidth()
void setWidth(int value)
}
class Square : Rectangle { }
现在我们遇到了一个问题,尽管界面匹配。原因是我们违反了由正方形和矩形的数学定义产生的不变量。 getter和setter的工作方式,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{}
罗伯特马丁有一个很棒的paper on the Liskov Substitution Principle。它讨论了可能违反原则的微妙而不那么微妙的方式。
本文的一些相关部分(注意第二个例子是高度浓缩的):
A Simple Example of a Violation of LSP
最明显违反此原则的行为之一是使用C ++运行时类型信息(RTTI)根据对象的类型选择函数。即:
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
的新衍生物时进行更改。实际上,许多人认为这个函数的结构是面向对象设计的诅咒。Square and Rectangle, a More Subtle Violation.
然而,还有其他更微妙的违反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
函数。这些功能完全不适合Square
,因为正方形的宽度和高度是相同的。这应该是设计存在问题的重要线索。但是,有一种方法可以回避这个问题。我们可以覆盖SetWidth
和SetHeight
[...]但请考虑以下功能:
void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth }
如果我们将对
Square
对象的引用传递给此函数,则Square
对象将被破坏,因为高度不会更改。这显然违反了LSP。该函数不适用于其参数的派生。[...]
LSP是必要的,其中一些代码认为它调用T
类型的方法,并且可能在不知不觉中调用S
类型的方法,其中S extends T
(即S
继承,衍生自或者是超类型T
的子类型)。
例如,这种情况发生在具有T
类型的输入参数的函数被调用(即调用)且参数值为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 setter期望维度被独立修改,但是不可变的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
不是逆变的w.r.t.到Ti
,然后一个子类型Xi
- 它不会是Si
的子类型 - 可以被分配给Ti
。
此外,对于在类型多态性参数(即泛型)上具有定义 - 位置方差注释的语言(例如Scala或Ceylon),类型T
的每个类型参数的方差注释的共同或反向必须是opposite或相同分别指向具有类型参数类型的每个输入参数或输出(T
的每个方法)。
另外,对于具有功能类型的每个输入参数或输出,所需的方差方向相反。此规则以递归方式应用。
Subtyping is appropriate可以枚举不变量。
关于如何建模不变量的研究正在进行中,因此它们由编译器强制执行。
Typestate(参见第3页)声明并强制执行与类型正交的状态不变量。或者,converting assertions to types可以强制执行不变量。例如,要在关闭文件之前声明文件已打开,则File.open()可以返回OpenFile类型,该类型包含File中不可用的close()方法。 tic-tac-toe API可以是使用类型在编译时强制执行不变量的另一个示例。类型系统甚至可以是图灵完整的,例如, Scala。依赖类型语言和定理证明器使高阶类型的模型形式化。
由于需要对abstract over extension进行语义化,我希望使用键入来模拟不变量,即统一的高阶指称语义,优于Typestate。 “扩展”是指无协调,模块化开发的无限制,置换组合。因为在我看来,它是统一的对立面,因而是自由度,有两个相互依赖的模型(例如类型和类型状态)来表达共享语义,这些模型不能相互统一以实现可扩展的组合。例如,类似Expression Problem的扩展在子类型,函数重载和参数类型域中统一。
我的理论立场是,对于knowledge to exist(参见“集中化是盲目和不合适”一节),永远不会有一个通用的模型可以在图灵完备的计算机语言中强制实现100%覆盖所有可能的不变量。为了存在知识,存在着意想不到的可能性,即无序和熵必须一直在增加。这是熵力。为了证明潜在扩展的所有可能计算,是计算先验所有可能的扩展。
这就是Halting定理存在的原因,即Turing完全编程语言中的每个可能的程序是否终止都是不可判定的。可以证明某些特定程序终止(所有可能性都已定义和计算)。但是不可能证明该程序的所有可能扩展都终止,除非扩展该程序的可能性不是图灵完成(例如通过依赖类型)。由于图灵完备性的基本要求是unbounded recursion,因此直观地理解哥德尔的不完备性定理和罗素的悖论如何适用于扩展。
对这些定理的解释将它们纳入对熵力的概括性概念理解中:
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是相同的。
使用指针或对基类的引用的函数必须能够在不知道它的情况下使用派生类的对象。
当我第一次阅读LSP时,我认为这是非常严格意义上的,基本上将其等同于接口实现和类型安全转换。这意味着LSP要么由语言本身确保,要么得到保证。例如,在严格意义上,就编译器而言,ThreeDBoard肯定可替代Board。
在阅读了更多关于概念的内容后,我发现LSP通常比这更广泛地被解释。
简而言之,客户端代码“知道”指针后面的对象是派生类型而不是指针类型意味着什么并不限于类型安全。通过探测对象的实际行为,也可以测试对LSP的遵守情况。也就是说,检查对象的状态和方法参数对方法调用结果的影响,或者从对象抛出的异常类型。
再回到示例,理论上可以使Board方法在ThreeDBoard上正常工作。然而,在实践中,很难防止客户端可能无法正确处理的行为差异,而不会影响ThreeDBoard要添加的功能。
掌握了这些知识,评估LSP依从性可以成为确定何时组合是扩展现有功能而不是继承的更合适机制的一个很好的工具。
有一个检查清单,以确定您是否违反Liskov。
清单:
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#开发人员)所以我不关心他们。
参考: