首页 文章

Liskov替代原则的例子是什么?

提问于
浏览
723

我听说Liskov替换原则(LSP)是面向对象设计的基本原则 . 它是什么以及它的使用例子是什么?

26 回答

  • 408

    一个很好的例子说明了LSP(我最近在一个播客中由Bob叔叔给出的)是有时在自然语言中听起来不对的东西在代码中不起作用 .

    在数学中, SquareRectangle . 实际上它是一个矩形的专业化 . "is a"使您希望使用继承对此进行建模 . 但是,如果在代码中 Square 派生自 Rectangle ,那么 Square 应该可以在任何预期 Rectangle 的地方使用 . 这会产生一些奇怪的行为 .

    想象一下,你的 Rectangle 基类上有 SetWidthSetHeight 方法;这似乎完全符合逻辑 . 但是,如果 Rectangle 引用指向 Square ,那么 SetWidthSetHeight 没有意义,因为设置一个将改变另一个以匹配它 . 在这种情况下 Square 使用 Rectangle 使Liskov替换测试失败,并且 SquareRectangle 继承的抽象是错误的 .

    你们应该看看其他无价的SOLID Principles Motivational Posters .

  • 39

    Liskov替换原则(LSP,lsp)是面向对象编程中的一个概念,它指出:

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

    从本质上讲,LSP是关于接口和 Contract 以及如何决定何时扩展一个类而不是使用另一个策略(如组合)来实现您的目标 .

    我所看到的最有效的方式来说明这一点是在Head First OOA&D . 他们提出了一个场景,您是一个项目开发人员,为战略游戏构建框架 .

    他们提出了一个代表董事会的类,如下所示:

    Class Diagram

    所有方法都将X和Y坐标作为参数,以在 Tiles 的二维数组中定位切片位置 . 这将允许游戏开发者在游戏过程中管理棋盘中的单元 .

    这本书继续改变要求,说游戏框架工作也必须支持3D游戏板,以适应有飞行的游戏 . 因此引入了 ThreeDBoard 类,扩展了 Board .

    乍一看,这似乎是一个很好的决定 . Board 提供 HeightWidth 属性, ThreeDBoard 提供Z轴 .

    当你看到从 Board 继承的所有其他成员时,它崩溃的地方 . AddUnitGetTileGetUnits 等方法都在 Board 类中同时获取X和Y参数,但 ThreeDBoard 也需要Z参数 .

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

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

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

  • 16

    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 的有效替代 .

  • 4

    LSP的这种表述过于强烈:

    如果对于类型S的每个对象o1,存在类型为T的对象o2,使得对于以T表示的所有程序P,当o1代替o2时P的行为不变,则S是T的子类型 .

    这基本上意味着S是另一个完全封装的与T完全相同的实现 . 我可以大胆并决定性能是P的行为的一部分......

    所以,基本上,任何后期绑定的使用都违反了LSP . 当我们将一种对象替换为另一种对象时,获得不同的行为是OO的重点!

    引用by wikipedia的表述更好,因为该属性取决于上下文,并不一定包括程序的整个行为 .

  • 15

    罗伯特马丁有一个很好的paper on the Liskov Substitution Principle . 它讨论了可能违反原则的微妙而不那么微妙的方式 .

    本文的一些相关部分(注意第二个例子是高度浓缩的):

    违反LSP的一个简单示例最明显违反此原则的一个原因是使用C运行时类型信息(RTTI)根据对象的类型选择函数 . 即:void DrawShape(const Shape&s)
    {
    if(typeid(s)== typeid(Square))
    DrawSquare(的static_cast <广场&>(S));
    否则如果(typeid(s)== typeid(Circle))
    画圆(的static_cast <圈&>(S));
    }
    显然,DrawShape函数形成错误 . 它必须知道Shape类的每个可能的衍生物,并且必须在创建Shape的新衍生物时更改它 . 实际上,许多人认为这个函数的结构是面向对象设计的诅咒 . 正方形和矩形,更微妙的违规 . 然而,还有其他更微妙的违反LSP的方式 . 考虑一个使用Rectangle类的应用程序,如下所述:class Rectangle
    {
    上市:
    void SetWidth(double w)

    void SetHeight(double h)

    double GetHeight()const {return itsHeight;}
    double GetWidth()const {return itsWidth;}
    私人的:
    加倍宽度;
    双倍的高度;
    };
    [...]想象一下,有一天用户需要能够操纵除矩形之外的方块 . [...]显然,正方形是所有正常意图和目的的矩形 . 由于ISA关系成立,因此将Square类建模为从Rectangle派生是合乎逻辑的 . [...] Square将继承SetWidth和SetHeight函数 . 这些函数完全不适合Square,因为正方形的宽度和高度是相同的 . 这应该是设计存在问题的重要线索 . 但是,有一种方法可以回避这个问题 . 我们可以覆盖SetWidth和SetHeight [...]但是请考虑以下函数:void f(Rectangle&r)
    {
    r.SetWidth(32); //调用Rectangle :: SetWidth
    }
    如果我们将Square对象的引用传递给此函数,则Square对象将被破坏,因为高度不会更改 . 这显然违反了LSP . 该函数不适用于其参数的派生 . [...]

  • 9

    可替代性是面向对象编程中的一个原则,指出在计算机程序中,如果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{}
    
  • 7

    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 的每个方法必须具有逆变输入参数和协变输出 .

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

    协方差意味着方差与继承的方向相同,即子类型 S 的每个方法的输出类型 So ,必须是相应方法的相应输出的 To 类型的相同或子类型 . 超级 T .

    这是因为如果调用者认为它具有 T 类型,则认为它正在调用 T 的方法,然后它提供 Ti 类型的参数并将输出分配给类型 To . 当它实际调用 S 的相应方法时,则每个 Ti 输入参数都分配给 Si 输入参数, So 输出分配给 To 类型 . 因此,如果 Si 不是逆变的,那么w.r.t.到 Ti ,然后是一个子类型 Xi -which不会是 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 . 依赖类型语言和定理证明器使高阶类型的模型形式化 .

    由于需要语义,我希望使用键入来模拟不变量,即统一的高阶指称语义,优于Typestate . “扩展”是指无协调,模块化开发的无限制,置换组合 . 因为在我看来,它是统一的对立面,因而是自由度,有两个相互依赖的模型(例如类型和类型状态)来表达共享语义,这些模型不能相互统一以实现可扩展的组合 . 例如,Expression Problem -like扩展在子类型,函数重载和参数类型域中统一 .

    我的理论立场是,对于knowledge to exist(参见“集中化是盲目的和不合适的”一节),永远不会有一个通用模型能够在图灵完备的计算机语言中强制实现100%覆盖所有可能的不变量 . 为了存在知识,存在着意想不到的可能性,即无序和熵必须一直在增加 . 这是熵力 . 为了证明潜在扩展的所有可能计算,是计算先验所有可能的扩展 .

    这就是Halting定理存在的原因,即Turing完全编程语言中的每个可能的程序是否终止都是不可判定的 . 可以证明某些特定程序终止(所有可能性都已定义和计算) . 但是不可能证明该程序的所有可能扩展都终止,除非扩展该程序的可能性不是图灵完成(例如通过依赖类型) . 由于图灵完备性的基本要求是unbounded recursion,因此直观地理解Gödel's incompleteness theorems and Russell's悖论如何适用于扩展 .

    对这些定理的解释将它们纳入对熵力的概括性概念理解中:

    • Gödel's incompleteness theorems :任何可以证明所有算术真理的形式理论都是不一致的 .

    • Russell's paradox :可以包含集合的集合的每个成员规则,枚举每个成员的特定类型或包含自身 . 因此集合既不能扩展也不能无限递归 . 例如,一套不是茶壶的东西,包括它自己,包括它自己,包括它自己等等 . 因此,如果规则(可能包含一个集合)并不枚举特定类型(即允许所有未指定的类型)并且不允许无界扩展,则规则是不一致的 . 这是一组不属于自己的集合 . 哥德尔的不完备性定理是无法在所有可能的扩展中保持一致和完全列举的 .

    • Liskov Substition Principle :通常,任何集合是否是另一个集合的子集是一个不可判定的问题,即继承通常是不可判定的 .

    • Linsky Referencing :在描述或感知事物的计算时,即感知(现实)没有绝对的参考点是不可判定的 .

    • Coase's theorem :没有外部参考点,因此无限外部可能性的任何障碍都将失败 .

    • Second law of thermodynamics :整个宇宙(一个封闭的系统,即一切)趋向于最大程度的无序,即最大的独立可能性 .

  • 117

    LSP是关于clases Contract 的规则:如果基类满足 Contract ,那么LSP派生类也必须满足该 Contract .

    在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是相同的 .

  • 2

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

    当我第一次阅读LSP时,我认为这是非常严格意义上的,基本上将其等同于接口实现和类型安全转换 . 这意味着LSP要么由语言本身确保,要么得到保证 . 例如,在严格意义上,就编译器而言,ThreeDBoard肯定可替代Board .

    在阅读了更多关于这个概念之后,我发现LSP通常比这更广泛地被解释 .

    简而言之,客户端代码“知道”指针后面的对象是派生类型而不是指针类型意味着什么并不限于类型安全 . 通过探测对象的实际行为,也可以测试对LSP的遵守情况 . 也就是说,检查对象的状态和方法参数对方法调用结果的影响,或者从对象抛出的异常类型 .

    回到示例, in theory Board方法可以在ThreeDBoard上正常工作 . 然而,在实践中,很难防止客户端可能无法正确处理的行为差异,而不会影响ThreeDBoard要添加的功能 .

    掌握了这些知识,评估LSP依从性可以成为确定何时组合是扩展现有功能而不是继承的更合适机制的一个很好的工具 .

  • 7

    奇怪的是,没有人发布描述lsp的原始paper . 这不像罗伯特·马丁那样易读,但值得 .

  • 20

    有一个检查清单,以确定您是否违反Liskov .

    • 如果您违反以下任何一项 - >您违反了Liskov .

    • 如果你不违反任何 - >无法做出任何结论 .

    清单:

    • No new exceptions should be thrown in derived class :如果您的基类抛出了ArgumentNullException,那么您的子类只允许抛出ArgumentNullException类型的异常或从ArgumentNullException派生的任何异常 . 抛出IndexOutOfRangeException是违反Liskov的 .

    • Pre-conditions cannot be strengthened :假设您的基类使用成员int . 现在你的子类型要求int为正数 . 这是强化前置条件,现在任何在使用负数之前完全正常工作的代码都会被破坏 .

    • Post-conditions cannot be weakened :假设您的基类需要在返回方法之前关闭与数据库的所有连接 . 在您的子类中,您覆盖该方法并保持连接打开以便进一步重用 . 你已经削弱了该方法的后置条件 .

    • Invariants must be preserved :要实现的最困难和痛苦的约束 . 不变量有时隐藏在基类中,揭示它们的唯一方法是读取基类的代码 . 基本上,您必须确保在重写方法时,在执行重写方法后,任何不可更改的内容都必须保持不变 . 我能想到的最好的事情是在基类中强制执行这种不变约束,但这并不容易 .

    • History Constraint :覆盖方法时,不允许修改基类中的不可修改属性 . 看看这些代码,您可以看到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件商品: Contravariance of method argumentsCovariance of return types . 但是在C#中我不可能(我是C#开发人员)所以我不关心他们 .

    参考:

  • 5

    使用LSP的一个重要例子是 software testing .

    如果我有一个类A是符合LSP的B类子类,那么我可以重用B的测试套件来测试A.

    为了完全测试子类A,我可能需要添加一些测试用例,但至少我可以重用所有超类B的测试用例 .

    一种实现的方法是通过构建McGregor所谓的"Parallel hierarchy for testing":我的 ATest 类将继承自 BTest . 然后需要某种形式的注入来确保测试用例适用于类型A而不是类型B的对象(简单的模板方法模式将会这样做) .

    请注意,为所有子类实现重用超级测试套件实际上是一种测试这些子类实现是否符合LSP的方法 . 因此,人们也可以争辩说,应该在任何子类的上下文中运行超类测试套件 .

    另请参阅Stackoverflow问题“Can I implement a series of reusable tests to test an interface's implementation?”的答案

  • 2

    我想每个人都在技术上涵盖了LSP:你基本上希望能够从子类型细节中抽象出来并安全地使用超类型 .

    所以Liskov有3个基本规则:

    • 签名规则:语法中子类型中的超类型的每个操作都应该有效实现 . 编译器可以为您检查的东西 . 关于抛出更少的异常并且至少像超类型方法一样可访问,有一个小规则 .

    • 方法规则:这些操作的实现在语义上是合理的 .

    • 较弱的前提条件:子类型函数应至少采用超类型作为输入的内容,如果不是更多 .

    • 更强的后置条件:它们应该生成超类型方法产生的输出的子集 .

    • 属性规则:这超出了单个函数调用 .

    • 不变量:始终如一的事物必须保持真实 . 例如 . Set的大小永远不会消极 .

    • 进化属性:通常与不变性或对象可以处于的状态有关 . 或者对象只能增长而不会缩小,因此子类型方法不应该成功 .

    需要保留所有这些属性,并且额外的子类型功能不应违反超类型属性 .

    如果这三件事都得到了解决,那么你已经从基础内容中抽象出来了,并且你正在编写松散耦合的代码 .

    资料来源:Java项目开发 - Barbara Liskov

  • 71

    Long 故事短,让我们留下矩形矩形和正方形方块,扩展父类时的实际示例,您必须保留确切的父API或EXTEND IT .

    假设你有一个 base 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并依赖它来使用 Client .

    /**
     * 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 在带有 sub class breaks the API's contractsubstituting parent 类时被破坏 .

    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://enterprise-level-php.com

  • 2

    一些附录:
    我想知道为什么没有人写关于派生类必须遵守的基类的不变量,前置条件和后置条件 . 要使派生类D完全可由Base类B维护,D类必须遵守某些条件:

    • 派生类必须保留基类的In-variants

    • 派生类不得强化基类的前提条件

    • 派生类不得削弱基类的后置条件 .

    因此派生必须知道基类强加的上述三个条件 . 因此,子类型的规则是预先确定的 . 这意味着,只有在子类型遵守某些规则时才应遵守'IS A'关系 . 这些规则以不变量,前提条件和后置条件的形式,应由正式的'design contract'决定 .

    我的博客上有关此问题的进一步讨论:Liskov Substitution principle

  • 3

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

    子类不得违反其基类特征 . 它必须有能力 . 我们可以说它与子类型相同 .

  • 47

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

    现在,子类型不能以相同的方式使用,因为它们不再产生相同的结果 .

  • 3

    正方形是一个矩形,其宽度等于高度 . 如果正方形为宽度和高度设置两个不同的大小,则它违反了方形不变量 . 这通过引入副作用来解决 . 但是如果矩形有一个setSize(高度,宽度),前提条件为0 <height和0 <width . 派生的子类型方法需要height == width;一个更强的先决条件(并且违反了lsp) . 这表明虽然square是一个矩形,但它不是一个有效的子类型,因为前提条件得到了加强 . 解决方法(通常是坏事)会导致副作用,这会削弱后置条件(违反lsp) . 基础上的setWidth具有post条件0 <width . 派生用height == width来削弱它 .

    因此,可调整大小的正方形不是可调整大小的矩形 .

  • -1

    Liskov的替换原则(LSP)我们一直在设计程序模块并创建一些类层次结构 . 然后我们扩展一些类创建一些派生类 . 我们必须确保新的派生类只是扩展而不替换旧类的功能 . 否则,新类在现有程序模块中使用时会产生不良影响 . Liskov的替换原则指出,如果程序模块使用Base类,则可以使用Derived类替换对Base类的引用,而不会影响程序模块的功能 .

    Example:

    以下是违反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的扩展,它意味着我们必须确保新的派生类扩展基类而不改变它们的行为 .

    另见: Open Close Principle

    更好结构的一些类似概念:Convention over configuration

  • 16

    在一系列董事会中实施ThreeDBoard会有用吗?

    也许您可能希望将各种平面中的ThreeDBoard切片视为Board . 在这种情况下,您可能希望抽象出Board的接口(或抽象类)以允许多个实现 .

    在外部接口方面,您可能想要为TwoDBoard和ThreeDBoard分配Board接口(尽管上述方法都不合适) .

  • 9

    假设我们在代码中使用矩形

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

    如果我们在第一个代码中用 Square 替换 Rectangle ,那么它将会破坏:

    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中"preconditions cannot be strengthened in a subtype"部分的示例 . 总而言之,违反LSP可能会在某些时候导致代码错误 .

  • 19

    我鼓励你阅读这篇文章:Violating Liskov Substitution Principle (LSP) .

    你可以在那里找到解释什么是Liskov替换原则,一般的线索可以帮助你猜测你是否已经违反了它,以及一个方法的例子,可以帮助你上课等级更安全 .

  • 4

    到目前为止我发现的LSP的最清楚的解释是"The Liskov Substitution Principle says that the object of a derived class should be able to replace an object of the base class without bringing any errors in the system or modifying the behavior of the base class"来自here . 本文给出了违反LSP并修复它的代码示例 .

  • 14

    LISKOV替换原则(来自Mark Seemann一书)指出,我们应该能够在不破坏客户端或实现的情况下将接口的一个实现替换为另一个 . 即使我们可以',这个原则也能够解决将来发生的需求 . 今天预见到他们 .

    如果我们从墙上拔掉电脑(实施),墙上插座(接口)和电脑(客户端)都不会发生故障(实际上,如果是笔记本电脑,它甚至可以在电池上运行一段时间) . 但是,对于软件,客户通常希望服务可用 . 如果删除了服务,我们会得到NullReferenceException . 为了处理这种情况,我们可以创建一个“什么也没做”的接口实现 . 这是一种称为Null Object的设计模式,[4]它大致相当于将计算机从墙上拔下来 . 因为我们正在使用松散耦合,所以我们可以用一些不会造成麻烦的东西替换真正的实现 .

  • 2

    Likov的替换原则指出,如果程序模块使用Base类,则可以用Derived类替换对Base类的引用,而不会影响程序模块的功能 .

    意图 - 派生类型必须完全替代其基本类型 .

    示例 - java中的Co-variant返回类型 .

  • 697

    以下摘录自this post,可以很好地澄清事情:

    [...]为了理解某些原则,重要的是要意识到它何时被违反 . 这就是我现在要做的 .

    违反这一原则意味着什么?它意味着一个对象不能满足用接口表示的抽象强加的 Contract . 换句话说,这意味着您确定了您的抽象错误 .

    请考虑以下示例:

    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的吗?是 . 这是因为该帐户的 Contract 告诉我们帐户将被撤销,但情况并非总是如此 . 那么,我该怎么做才能解决它?我只是修改 Contract :

    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à,现在 Contract 很满意 .

    这种微妙的违规通常会使客户能够分辨出所使用的具体对象之间的区别 . 例如,根据第一个帐户的 Contract ,它可能如下所示:

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

    并且,这自动违反了开放式原则[即,对于提款要求 . 因为如果违反 Contract 的对象没有足够的钱,你永远不会知道会发生什么 . 可能它只返回任何内容,可能会抛出异常 . 所以你必须检查它是否 hasEnoughMoney() - 这不是一个接口的一部分 . 所以这个强制的具体类依赖检查是OCP违规] .

    这一点也解决了我经常遇到的关于LSP违规的错误观念 . 它说“如果父母的行为在孩子身上发生了变化,那么就会违反LSP . ”然而,只要孩子没有违反其父母的 Contract ,它就不会 .

相关问题