zoukankan      html  css  js  c++  java
  • 敏捷软件开发:原则、模式与实践——第10章 LSP:Liskov替换原则

    第10章 LSP:Liskov替换原则   

      Liskov替换原则:子类型(subtype)必须能够替换掉它们的基类型(base type)。


    10.1 违反LSP的情形

    10.1.1 简单例子

      对LSP的违反导致了OCP的违反:

    struct Point { double x, y;}
    public enum ShapeType { square, circle };
    public class Shape
    {
        private ShapeType type;
        public Shape(ShapeType t) { type = t; }
        public static void DrawShape(Shape s)
        {
            if (s.type == ShapeType.square)
                (s as Square).Draw();
            else if (s.type == ShapeType.circle)
                (s as Circle).Draw();
        }
    }
    public class Circle : Shape
    {
        private Point center;
        private double radius;
        public Circle() : base(ShapeType.circle) { }
        public void Draw() {/* draws the circle */}
    }
    public class Square : Shape
    {
        private Point topLeft;
        private double side;
        public Square() : base(ShapeType.square) { }
        public void Draw() {/* draws the square */}
    }

      很显然DrawShape函数违反了OCP。它必须知道Shape类每个可能的派生类,并且每次创建一个Shape类派生出的新类时都必须要更改它。


    10.1.2 更微妙的违反情形

      下面是一个Rectangle类型:

    public class Rectangle
    {
        private Point topLeft;
        private double width;
        private double height;
        public double Width
        {
            get { return width; }
            set { width = value; }
        }
        public double Height
        {
            get { return height; }
            set { height = value; }
        }
    }

      某一天,用户要求添加正方形的功能。

      我们经常说继承是IS-A(是一个)关系。从一般意义上讲,一个正方形就是一个矩形。因此把Square类视为从Rectangle类派生是合乎逻辑的。不过,这种想法会带来一些微妙但几位值得重视的问题。一般来说,这些问题是很难遇见的,直到我们编写代码时才会发现。

      Square类并不同时需要height和width。但是Square仍会从Rectangle中继承它们。显然这是浪费。假设我们不十分关心内存效率。写出如下自相容的Rectangle类和Square类代码:

    public class Rectangle
    {
        private Point topLeft;
        private double width;
        private double height;
        public virtual double Width
        {
            get { return width; }
            set { width = value; }
        }
        public virtual double Height
        {
            get { return height; }
            set { height = value; }
        }
    }
    public class Square : Rectangle
    {
        public override double Width
        {
            set
            {
                base.Width = value;
                base.Height = value;
            }
        }
        public override double Height
        {
            set
            {
                base.Height = value;
                base.Width = value;
            }
        }
    }

    真正的问题

      现在Square和Rectangle看起来都能够工作。这样看起来该设计似乎是自相容的、正确的。可是,这个结论是错误的。一个自相容的设计未必就和所有的用户程序相容。考虑如下函数:

        void g(Rectangle r)
        {
            r.Width = 5;
            r.Height = 4;
            if (r.Area() != 20)
                throw new Exception("Bad area!");
        }

      对于Rectangle来说,此函数运行正确,但是,如果传递进来的是Square对象就会抛出异常。所有,真正的问题是:函数g的编写者假设改变Rectangle的常不会导致宽的改变。

      显然,改变一个长方形的宽不会影响他的长是的假设是合理的!然而,并不是所有作为Rectangle传递的对象都满足这个假设。函数g对于Square、Rectangle层次结构来说是脆弱的。对于g来说,Square不能替换Rectangle,因此Square和Rectangle之间的关系是违反LSP的。

    有效性并非本质属性

      一个模型,如果孤立的看,并不具有真正意义上的有效性。模型的有效性只能通过它的客户程序来表现。因此,像其他原则一样,只预测那些最明显的对于LSP的违反的情况而推迟所有其他的预测,直到出现相关的脆弱性的臭味时,才去处理它们。

    ISA是关于行为的

      OOD中IS-A关系是就行为方式而言的,行为方式是可以进行合理假设的,是客户程序所依赖的。


    10.2 用提取公共部分的方法代替继承

    查看如下代码:

    public class Line
    {
        private Point p1;
        private Point p2;
        public Line(Point p1, Point p2) { this.p1 = p1; this.p2 = p2; }
        public Point P1 { get { return p1; } }
        public Point P2 { get { return p2; } }
        public double Slope { get {/*code*/} }
        public double YIntercept { get {/*code*/} }
        public virtual bool IsOn(Point p) {/*code*/}
    }
    
    public class LineSegment : Line
    {
        public LineSegment(Point p1, Point p2) : base(p1, p2) { }
        public double Length() { get {/*code*/} }
        public override bool IsOn(Point p) {/*code*/}
    }

      初看,会觉得它们之间自然有继承关系。但是,这两个类还是以微妙的方式违反了LSP。

      Line的使用者可以期望和该Line具有线性线性对应关系的所有点都在该Line上。例如,由YIntercept属性返回的点就是线和轴的交点。由于这个点和线具有线性对应关系,所以Line的使用者可以期望IsOn(YIntercept())==true。然而,对于许多LineSegment的实例,这条声明会失效。

      一个简单的方案可以解决Line和LineSegment的问题,该方案也阐明了一个OOD的重要工具。如果我们可以同时具有Line类和LineSegment类的访问权限,那么可以把这两个类的公共部分提出来一个抽象基类。如下:

    public abstract class LinearObject
    {
        private Point p1;
        private Point p2;
        public LinearObject(Point p1, Point p2)
        { this.p1 = p1; this.p2 = p2; }
        public Point P1 { get { return p1; } }
        public Point P2 { get { return p2; } }
        public double Slope { get {/*code*/} }
        public double YIntercept { get {/*code*/} }
        public virtual bool IsOn(Point p) {/*code*/}
    }
    
    public class Line : LinearObject
    {
        public Line(Point p1, Point p2) : base(p1, p2) { }
        public override bool IsOn(Point p) {/*code*/}
    }
    
    public class LineSegment : LinearObject
    {
        public LineSegment(Point p1, Point p2) : base(p1, p2) { }
        public double GetLength() {/*code*/}
        public override bool IsOn(Point p) {/*code*/}
    }

      提取公共部分是一个有效的工具。如果两个类中有一些公共的特性,那么很可能稍后出现的其他类也会要这些特性。例如Ray类:

    public class Ray : LinearObject
    {
        public Ray(Point p1, Point p2) : base(p1, p2) {/*code*/}
        public override bool IsOn(Point p) {/*code*/}
    }


    10.3 启发式规则和习惯用法

      完成的功能少于基类的派生类通常是不能替换其类的,因此就违反了LSP。

      查看如下代码:

    public class Base
    {
        public virtual void f() {/*some code*/}
    }
    public class Derived : Base
    {
        public override void f() { }
    }

      在Base中实现了函数f。不过,在Derived中,函数f是退化的。也许,Derived的编程者认为函数f在Derived中没有用处。遗憾的是,Base的使用者不知道他们不应该调用f,因此就出现了一个替换违规。

      在退化类中存在退化函数并不总是表示违反了LSP,但是当存在这种情况时,还是值得注意一下的。


    10.4 结论  
      OCP是OOD中很多说法的核心。LSP是使OCP成为可能的主要原因之一。
      术语IS-A的含义过于宽泛以至于不能作为子类型的定义。子类型的正确定义是可替换的。

    摘自:《敏捷软件开发:原则、模式与实践(C#版)》Robert C.Martin    Micah Martin 著

    转载请注明出处:

    作者:JesseLZJ
    出处:http://jesselzj.cnblogs.com

  • 相关阅读:
    Unique Binary Search Trees——LeetCode
    Binary Tree Inorder Traversal ——LeetCode
    Maximum Product Subarray——LeetCode
    Remove Linked List Elements——LeetCode
    Maximum Subarray——LeetCode
    Validate Binary Search Tree——LeetCode
    Swap Nodes in Pairs——LeetCode
    Find Minimum in Rotated Sorted Array——LeetCode
    Linked List Cycle——LeetCode
    VR AR MR
  • 原文地址:https://www.cnblogs.com/jesselzj/p/4764766.html
Copyright © 2011-2022 走看看