我已经听说里氏替代原则(LSP)是一项基本原则面向对象的设计。它是什么和什么是一些例子,其使用?

有帮助吗?

解决方案 2

里氏替代原则(LSP, )是一个概念在对象的节目导向,即各国:

功能,使用的指针,或 引用基类必须 能够使用对象的生类 不知道。

在它的心脏LSP是关于接口和合同以及如何决定何时延长一个类比.使用另一种策略,例如组合来实现自己的目标。

最有效的方法我已经看到为了说明这一点是在 头先使&D.他们前一种情况,你是一个开发商在一个项目,建立一个框架的战略游戏。

他们本的一类,它表示一个委员会,是这样的:

Class Diagram

所有的方法采取X和Y坐标作为参数,以找到瓷砖位置在两个维阵列 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。

其他提示

一个很好的例子,说明LSP(由鲍勃叔叔在一个播客的我最近听到)是有时候东西听起来就在自然语言并不相当的工作。

在数学, Square 是一个 Rectangle.事实上,它是一个专业化的矩形。"是一个"让你想模型,这与继承。然而,如果在代码你了 Square 来自 Rectangle, 然后一个 Square 应该可以使用的任何地方,你期望的 Rectangle.这使得对于一些奇怪的行为。

想象你了 SetWidthSetHeight 方法上你 Rectangle 基类;这似乎是完全合乎逻辑的。但是如果你的 Rectangle 参考指出, Square, 然后 SetWidthSetHeight 没有意义,因为设置一个会改变的其他相匹配。在这种情况下 Square 失败的里氏的替代测试 Rectangle 和抽象的具有 Square 继承 Rectangle 是一个坏的。

enter image description here

你们应该看看其他无价的 固体原则的激励海报.

LSP关切不变量。

经典的例子是通过下伪码宣言》(实现省略):

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

class Square : Rectangle { }

现在我们有一个问题,虽然接口相匹配。其原因是,我们侵犯了不变量所产生的数学定义的方和矩形。这种方式吸气,并制定者的工作, Rectangle 应该满足以下固定:

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

然而,这不变 必须 被侵犯了通过执行正确的 Square, 因此它不是一个有效的替代品 Rectangle.

可替代性是一个原则,在面向对象编程指出,在一个计算机程序,如果是一个子类型的T,然后对象T类型的可替换物的类型S

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

坏例子

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

鸭子可以飞的,因为它是一只鸟,但这是什么:

public class Ostrich extends Bird{}

鸵鸟一只鸟,但它不能飞,鸵鸟类和子类型的鸟类,但它不能使用的飞行方法,这意味着我们打破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<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(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 功能。这些 职能是完全不合适的 Square, ,由于宽和 高度的一个方是相同的。这应该是一个重大线索 是有问题的设计。然而,有一个方法 回避的问题。我们可以重写 SetWidthSetHeight [...]

但是,考虑下列功能:

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

甚至一个种类型与 不可改变的领域 仍然不变的,例如的 不可改变的 矩形制定者预期的尺寸可以独立地修改,但的 不可改变的 方制定者违反了这一期望。

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 必须有逆变的输入参数(s)和一个协变的产出。

逆变的装置出现差异是相反方向的继承,即的类型 Si, 每个输入参数的每一种方法的型 S, 必须以相同或 超类型 的类型 Ti 相应的输入参数的相应方法的超类型 T.

协装置出现差异,是同一方向的继承,即的类型 So, 的输出每种方法的型 S, 必须以相同或 亚型 的类型 To 相应的输出相应的方法的超类型 T.

这是因为如果呼叫者认为它具有类型 T, ,认为它是调用的方法 T, 然后它提供的参数(s)的类型 Ti 分配的输出类型 To.当它实际上是呼吁相应的方法 S, 然后每 Ti 输入参数分配给一个 Si 输入参数, So 输出分配给类型 To.因此,如果 Si 是不逆变w。r.t.要 Ti, 然后一个子类型 Xi—这不会是一个子类型的 Si—可能是分配给 Ti.

此外,对于语言(例如斯卡拉或斯里兰卡),它有义内的差异上的注释型态性参数(即仿制药)时,共同或反方向上的差异,注释每种类型参数的类型 T 必须 相反的 或同样的方向分别向每一个输入参数或输出(每种方法的 T)具有类型的类型参数。

此外,每个参数输入或输出,有一个功能类型的差异的方向所需要的是相反的。这规则应用递归。


子类型是合适的 这里不变,可以列举。

有许多正在进行的研究关于如何模型不变,以便使它们强制执行通过的编译器。

Typestate (见第3页)宣布和强制执行状态变量正交的类型。或者,不变,可以强制执行 转换的主张类型.例如,为了维护这一文件开放,在关闭之前它,然后的文件。open()可以返回-&gt;打开文件类型,其中包含一个close()方法就是不提供的文件。一个 井字游戏API 可以是另一个例子雇用打字,以执行固定在编译时间。这类系统甚至可能是图灵完成的,例如 斯卡拉.依赖型的语言和定理校准仪式化的模型的高阶打字。

由于需要对语义 抽象在扩展, 我希望,雇用打字模型不变,即统一的更高阶denotational语义,是优于Typestate.'扩展'系指无界,置换的组成不协调的模块化的发展。因为它对我来说似乎是对立面的统一,并因此自由度,具有两个相互依赖的模型(例如类型和Typestate)表达的共同语义,这不可能是统一与每一其他可扩展的组成。例如, 表达的问题-像是统一的子类型、功能负荷超载,和输入参数的领域。

我的理论认为 知识存在 (见部分"集中化,是盲目和不适宜的"),就会 从来没有 是个一般的模式,可以强制执行100%的复盖范围的所有可能固定在一个灵完成的计算机语言。对知识的存在,意外的可能性,如存在,即障碍和熵必须始终在增加。这是熵力。来证明所有可计算的一个潜在的扩展,是以计算的先验的所有可能的扩展。

这就是为什么停止定存在,即它是无法判定是否每一个可能的程序在灵完成编程语言的终止。它可以证明,一些具体的程序终止(一个其所有的可能性已被定义和计算).但它是无法证明所有可能的扩展,该程序将终止,除非有可能性扩展这一程序不是图灵完成(例如通过从属的打字).由于基本要求图灵完整性 无限的递归, 它是直观了解多哥德尔就是不完整的定理和Russell的矛盾应用到扩展。

一个解释这些定理纳入他们在一个广义的概念性理解的熵力:

  • 哥德尔就是不完整的定理:任何正式理论,其中所有算法的真理可以证明,是不一致的。
  • Russell是个悖论:每一会员国规则,设定可以包含一个设置,列举了特定类型的每个成员或者包含本身。因此,设置不可延长,或者他们是无限的递归。例如,设置的一切,不是一个茶壶,包括本身,其中包括本身,其中包括本身,等等。这样一项规则是不一致的,如果它(可能含有的一套和)没有列举具体类型(即允许所有未说明的类型),并且不允许无限的扩展。这是一组组成员自己。这个无法以一致和完全列举的所有可能的扩展,是哥德尔的不完整定理。
  • 里氏Substition原则:一般来说它是一个无法判定的问题是否有任何集的子另一个,即继承一般无法判定的。
  • 林斯基引用:它是无法判定什么算什么,当它被描述的或感知的,即感知(现实)没有绝对的参考点。
  • 科斯定理:没有任何外部参考点,因此任何障碍,以限制外部的可能性将失败。
  • 热力学第二定律:整个宇宙(一个封闭的系统,即一切都)的趋势对最大障碍,即最大的独立的可能性。

LSP是一规则有关的合同的类:如果一个基类满足了一份合同,然后由LSP衍生的课程也必须满足这一合同。

在伪蟒蛇

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

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

满足LSP如果你每次呼叫Foo上派生的对象,它提供了完全相同的结果作为调Foo在一个底座的对象,只要参数是相同的。

功能,使用的指针,或提到基类必须能够使用对象的源类别而不自知。

当我第一次读约LSP,我认为这是意味着在一个非常严格意义上说,实质上等同它接口的实施和类型安全铸造。这将意味着LSP是既保证不了的语言本身。例如,在这种严格的意义上,ThreeDBoard肯定是可取代董事会,作为编译器而言。

在阅读了更多关于该概念,尽管我发现,LSP是一般解释更广泛。

总之,它意味着什么的客户代码"知道"这就象后面的指针是一个衍生出类型,而不是指针的类型没有限制类型的安全。遵守LSP还可测试的,通过探测的物体的实际行为。就是说,在审查所产生的影响对象的国家和方法论上的结果的方法的呼吁,或类型的例外情况引发的对象。

回去的例子再一次, 在理论上 委员会的方法可以做的工作只是现在ThreeDBoard.但是在实践中,这将是非常困难,以防止的行为差异,客户可能不正确处理,而步履蹒跚的功能,ThreeDBoard旨在增加。

与这些知识在一方面,评估LSP遵守可以是一个伟大的工具在确定当组成的更适当机制,扩大现有的功能,而不是继承。

有一个核对表以确定是否违反了里氏.

  • 如果你违反了下列一个项目-->你违反了里氏.
  • 如果你不违反任何>不能缔结任何东西。

检查清单:

  • 没有新的例外情况应该被扔在源类:如果你的基类扔了基础,然后你的子类,只允许扔掉例外情况类型的基础或任何例外情况源自基础的.扔IndexOutOfRangeException是违反了里氏.
  • 预的条件下不能被加强了:假设你的基类的工作与成员int。现在你的子类型的要求int是积极的。这是加强预的条件下,和现在的任何代码的完美工作的现之前的负面整数被破坏。
  • 后条件下不能被削弱:假设你的基本类所需的所有连接的数据库应该是关闭以前的方法返回。在你的子类推翻了这一方法及阔叶连接打开进一步的再利用。你已经被削弱的后条件的方法。
  • 不变必须保留的:最困难和最痛苦的约束,以履行。不变的是一些时间隐藏在基类的唯一方法来揭示它们是读码的基类。基本上你有一点可以肯定当你替代方法的任何改变必须保持不变之后,你的复盖的方法执行。最好的事情,我可以认为是执行这个不变的制约因素基本类的但是这不会是容易的。
  • 历史约束:当压倒一切的一个方法你都不允许修改联合国可修改财产在基类。看看这些代码,你可以看到名字的定义是以联合国可改变的(私人设定的),但型引入了新的方法,允许修改(通过反射):

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

参考:

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

如果我有一个类,这是一个LSP符合子类的B,然后我可以重复使用的测试套B测试A.

到充分测试一个子类,我也许需要添加更多的几个试验案件,但至少我可以重复使用所有的B超类的测试案例。

一方式来实现,这是通过建立什么McGregor谓的"平行层次的用于测试":我 ATest 类会继承 BTest.某种形式的注射,然后需要确保试验的情况下适用对象类型的一个,而不是类型为B(一个简单的模板的方法模式将做)。

注意重复使用超级测试为所有类实现事实上是一种试验,这些子类实现LSP符合。因此,人们还可以认为,一个 应该 运行超类测试的上下文中的任何子类。

还请参见答案的计算器问题"我可以执行一系列可重复使用的测试,以测试一个接口的实施?"

我想每个人都种复盖什么LSP在技术上是:你基本上希望能够抽象离子类型的详细信息,并使用超类型的安全。

那里氏有3个基本规则:

  1. 签名的规则:应该有一个有效的执行的每一动作超类型的子类型的语法上.什么一个编译器,将能够检查。有一个小小的规则有关的投掷更少的例外情况,至少作为访问作为超类型的方法。

  2. 方法的规则:执行这些行动是语义上的声音。

    • 较弱的先决条件:该子类型的职能应至少需要什么超类型作为投入,如果不是更多。
    • 更强大的后置条件:他们应该产生一个子集的输出超类型的方法产生的。
  3. 性能的规则:这超出了个别功能的电话。

    • 固定:事情总是如此,必须保持真实的。例如。一组是大小从来没有负面的。
    • 进化特性:通常是与不可变性或种类的国家的对象可以是。或者,也许对象仅增长并且从来没有缩小,那么该子类型的方法不应该使它。

所有这些性质需要保留和额外的子类型的功能不应违反超类型的性质。

如果这些三件事情是照顾,你有抽离根本的东西和你正在写的松散耦合的代码。

资料来源:程序发展Java-芭芭拉里氏

总而言之,让我们离开矩矩形和方块的平方,实际的例子,当延伸父类,你必须保存的确切父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;
    }
}

然后你能有一个 客户 工作与基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是太强:

如果对每个对象o1类型S还有一个目的是o2T类型,例如,对于所有程序P定义方面的T的行为P不变当o1代o2,然后是亚型的T

这基本上意味着这是另一个完全封装实施的同样的事情如T.我可以大胆的决定性一部分的行为。

所以,基本上,任何使用的延迟结合违反LSP。它是整点的OO以获得一个不同的行为当我们的替代品的一个目的一个种类之一的另一种!

制定引 维基百科 最好是由于性取决的上下文并不一定包括整个行为的程序。

一些增编:
我不知道为什么没人写有关不变的,先决条件和后条件的基类,必须遵守的衍生课程。对于一种衍生的D类是完全sustitutable由基B类、D类必须遵守某些条件:

  • 在变的基类必须保留由的派生类
  • 预先条件的基类必须不加强的源类
  • 后条件的基类必须不被削弱的源类。

所以得出必须知道的上述三个条件规定的基类。因此,规则的子类型都预先决定。这意味着,一个关系应遵守只有当某些规则的遵守该子类型。这些规则,在形式不变量,precoditions和后置条件,应当由一个正式的'设计合同'.

进一步讨论这一在我的博客: 里氏替换原则

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

儿童类必须不违反其基类的特性。它必须能够用它。我们可以说是同型.

我看到矩形和平方在每一个答案,以及如何违反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 !
    }
}

现在的亚型的无法使用同样的方式,因为它们不能产生同样的结果了。

里氏的替代原则(LSP)

所有的时间,我们设计一个模块的程序和我们创建一些类 层级结构。然后我们延长某些类创造一些衍生的 课程。

我们必须确保新得出类只是延长没有 更换功能的老旧的课程。否则,新的课程 可以产生期望的效果时,他们使用现有的程序 模块。

里氏的替代原则的国家,如果一个程序模块 使用一个基类,然后参照基类可以 换一个源类不影响功能 该程序模块。

例如:

以下是典型的例子为这里氏的替代原则被违反。在该实例中,2类使用:矩形和广场。让我们假定该矩形对象是使用某处在应用程序。我们扩展应用和增添的方类。该方类是返回的一个工厂案,根据某些条件下,我们不知道确切的什么类型的对象将是返回。但我们知道这是一个矩形。我们的矩形象,设置宽度为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.
    }
}

结论:

这项原则仅仅是一个扩大的开放关闭的原则和它 意味着我们必须确保新得出类延伸 基类不改变自己的行为。

参见: 开放关闭的原则

一些类似概念的更好的结构: 《公约》在配置

将执行ThreeDBoard方面的一系列事会是有用吗?

也许您可能想要把切片的ThreeDBoard在各种飞机,作为一个委员会。在这种情况下,你可能要摘掉一个接口(或抽象的类)的董事会,以允许用多种方式实现的。

在外部接口,可能要因素,董事会界面对的两TwoDBoard和ThreeDBoard(尽管没有上述的方法配合)。

一个正方形是矩形,宽度等于高度。如果方设定两种不同大小的宽度和高度,它违反了平不变。这是工作围绕通过引入副作用。但是,如果矩形有setSize(高度、宽度)与的先决条件0 < 高度和0 < 宽度。所衍生的子类型的方法要求高度==宽度;一个更坚强的先决条件(和违反lsp)。这表明,虽然正方形是矩形它不是一个有效的子类,因为前提条件是得到加强。工作周围(一般是坏事)造成的副作用,这就削弱了后的状况(这违反了lsp)。setWidth的基础已经后状况0 < 宽度。所衍生的削弱它与高度==宽度。

因此,一个可调整的方不是一个可调整的长方形。

让我们说我们使用一个矩形在我们的代码

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

如果我们取代的 RectangleSquare 在我们的第一个代码然后它会破坏:

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 实例,并以它们会导致意外错误,在你的代码。

这是一个例子 "先决条件无法以加强在一个子类" 部分的 维基的文章.总而言之,违反了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()无法实现的。

这些都是各种各样的问题,违反了里氏替代 原则导致,他们可以最通常被认可的一个 方法,什么也不做,或甚至不可能实现的。

解决这些问题是一个正确的继承的层次结构,并在我们的情况下,我们会解决这个问题通过区分类的运输设备,并没有引擎。虽然一辆自行车是一个运输设备,也没有一个引擎。在这个例子我们一定义的运输设备是错误的。它不应该有一个引擎。

我们可以重构,我们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() { ... }
}

因此,我们的车类变得更加专业化,同时坚持里氏替代原则。

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

和我们的自行车的类也是在遵守的里氏替代原则。

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

我鼓励你读文章: 违反了里氏替代原则(LSP).

你可以找到有一个解释什么是里氏替代原则,一般事的线索帮助你猜如果你已经违反了它的一个例子办法,这将帮助你让你的类层次将更加安全。

最明显的解释LSP我发现迄今为止已经"里氏替代原则上说,对象的一个源类应当能够取代一个目的的基本类没有带来任何错误。在系统或修改行为的基类"从 在这里,.本文码例违反LSP和固定它。

里氏替代原则(从马昆特的书)指出,我们应当能够取代一个执行情况的一个接口,另一个不破坏无论是客户或实施。这是这一原则,能够解决的要求,在未来发生,甚至如果我们不能预见到他们今天。

如果我们拔掉电脑离墙(执行),也不墙上插座(接口),也没有计算机(Client)打破了(事实上,如果这是一个膝上型计算机,甚至可以运行在其电池一段时间)。与软件,但是,客户通常期望的服务可用。如果该服务已被删除,我们得到一个NullReferenceException.处理这种情况,我们可以创建一个实施的一个接口,不"什么"。 这是一个设计图案称为Null目的,[4]和它大致对应于拔掉电脑离墙。因为我们正在使用的松散耦合,我们可以代替真正的实现的东西,什么也不做,而不会造成麻烦。

Likov的替代原则的国家 如果程序模块,为使用一个基类,然后参照基类可以替换为一个源类不影响功能的程序模块。

意图来源的类型必须完全的替代品能够用于它们的基类型。

例-的共变量回归类型。

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();

例子取自 在这里,

这里是一个摘录 这个职位 这澄清了的东西很好:

[..]为了理解一些原则,重要的是要认识到,当它受到侵犯。这是什么我现在要做的。

什么违反了这一原则的意思吗?这意味着,一个对象不履行合同规定由一个抽象的概念表示有一种接口。换句话说,它意味着你确定你的抽象概念是错误的。

考虑下面的例子:

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

瞧,现在的合同感到满意。

这种细微的违规通常规定对客户的能力之间的差异具体对象采用。例如,给予第一账户的合同,它可能看起来像下:

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

而且,这会自动违反了打开封闭的原则,[即,对提款的要求。因为你永远不知道会发生什么,如果对象违反该合同没有足够的资金。可能它只是返回什么都没有,大概一个例外将会被抛出。所以你必须要检查,如果它 hasEnoughMoney() -这不是部分的一个接口。因此,这迫的具体类依赖的检查是一个路电违反].

这一点也解决了一种误解,我收到很多关于LSP违反。它说,"如果父母的行为改变一个儿童,那么,它违反了LSP。" 然而,它没有—只要一个孩子并不违反其父母的合同。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top