代码改变世界

OOP的几个原则-----LSP:Liskov替换原则(上)

2012-03-08 00:54  宅的一米  阅读(374)  评论(0编辑  收藏  举报

LSP的定义是:子类型必须能够替换掉它们的基类型.

OCP主要的机制是抽象和多态.而LSP探讨的问题是如何构建最佳的集成层次,它们的特征是什么?如何避免使我们创建的类层次结构掉进不符合OCP的陷阱中去.

Barbara Liskov在1988年首次提出这个原则:

若对类型S的每一个对象O1,都存在一个类型T的对象O2,使得在所有针对T编写的程序P中,用O1替换O2后,程序P的行为功能不变,则S是T的子类型.

按照这个原则我们可以大致推导出这样一段代码:

 

View Code
class Program
    {
        static void Main(string[] args)
        {
            S o1 = new S();
            P.Show(o1);
            Console.Read();

        }
    }

    class S:T
    {
        public override void Show()
        {
            Console.WriteLine("S");
        }
    }

    class T
    {
        public virtual void Show()
        {
            Console.WriteLine("T");
        }
    }

    class P
    {
        public static void Show(T o2)
        {
            o2.Show();
        }
    }

可以想象一下不符合这个原则的场景.假设把O1作为T类型的传递给P程序中的Show函数,会导致Show函数出现错误的行为.那么为了纠正这个错误,我们可能会将O2参数转换成S类型,对具体的类进行操作.此时Show函数对于T类型的所有派生类型都不在是封闭的.无形中我们就违反了OCP原则.通常违反LSP的情形都是比较微妙的.比如来思考一下这个问题:

还是一个图形的问题...假设我们的应用程序中有一个矩形(Rectangle).用户可以设置它的长度和宽度,可以设置它的左上点的坐标.并且会有其他的类能够绘制它们.简单的代码应该是这样

View Code
    public class Rectangle
    {
        private Point topLeft = new Point();

        private double width;
        private double height;

        public double Width
        {
            get { return width; }
            set { width = value; }
        }

        public double Height
        {
            get { return height; }
            set { height = value; }
        }

        public Rectangle()
        {

        }

        public void SetTopLeft(int x, int y)
        {
            topLeft.X = x;
            topLeft.Y = y;
        }

        public void SetWidth(double width)
        {
            Width = width;
        }

        public void SetHeight(double height)
        {
            Height = height;
        }
    }

    public class Point
    {
        public int X { getset; }
        public int Y { getset; }
    }

 

客户总是多变的.某天他们需要能够绘制正方形 (Square).从一般意义上来说,正方形就是一个特殊的矩形.对吧,初中老师都是这么说的.表面上看,Square Is-A Rectangle,这种关系代表这继承.Square派生自Rectangle.目前来说,这种扩展是没有问题的.可是....编程时我们却发现,Square类的长度和宽度都应该是一样的.但是从Rectangle继承而来的属性却无法满足这个条件.这是存在问题的重要标志,不过我们可以用其他途径解决,比如说这样:

View Code
public class Square : Rectangle
    {
        public Square()
        {

        }

        public new double Width
        {
            set
            {
                base.Width = value; 
                base.Height = value;
            }
            get { return base.Width; }
        }

        public new double Height
        {
            set
            {
                base.Height = value;
                base.Width = value;
            }
            get { return base.Height; }
        }

        public new void SetWidth(double width)
        {
            Width = width;
        }

        public new void SetHeight(double height)
        {
            Height = height;
        }
    }

所有在Rectangle类中修改宽度和高度属性的方法我们都要重写!!!更别提只修改私有字段的地方...不过你一通奋改之后,应该可以保证Square类确实是Square,Rectangle确实是Rectangle.下面这个测试可以保证是这样:

View Code
 [TestFixture]
    public class RectangleTest
    {
        [Test]
        public void Test()
        {
            Square s = new Square();

            s.SetHeight(10.00);

            Assert.AreEqual(10.00, s.Width);

            s.SetWidth(20.00);

            Assert.AreEqual(20.00, s.Height);
            Assert.AreEqual(s.Width, s.Height);

            Rectangle r = s;

            r.SetHeight(30.00);

            Assert.AreEqual(20.00, s.Width);
            Assert.AreEqual(30.00, s.Height);
            Assert.AreNotEqual(r.Height, r.Width);

            r.SetWidth(40.00);

            Assert.AreEqual(30.00, s.Height);
            Assert.AreEqual(40.00, s.Width);
            Assert.AreNotEqual(r.Height, r.Width);
        }
    }

不过可能恐怖的事情远比我们想的还要多...看看这下面这段代码:

View Code
 public void f(Rectangle r)
        {
            r.SetWidth(15.80);
        }

如果我们现在向这个函数传递一个指向Square对象的引用,这个Square对象就会遭到破坏,因为它的长度肯定不会改变.这显然违反了LSP.或许你可以将问题归咎于在Rectangle类中一些属性和方法没有设置virtual,然而如果派生类的创建导致我们修改基类,这常常意味着设计是有缺陷的,当然也违反了OCP.可是真的设置为virtual就能避免再次发生类似的问题了吗?

View Code
  public class Rectangle
    {
        private Point topLeft = new Point();

        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 Rectangle()
        {

        }

        public void SetTopLeft(int x, int y)
        {
            topLeft.X = x;
            topLeft.Y = y;
        }

        public void SetWidth(double width)
        {
            Width = width;
        }

        public void SetHeight(double height)
        {
            Height = height;
        }

        public double Area()
        {
            return Height * Width;
        }
    }

 

View Code
public class Square : Rectangle
    {
        public Square()
        {

        }

        public override double Width
        {
            set
            {
                base.Width = value;
                base.Height = value;
            }
            get { return base.Width; }
        }

        public override double Height
        {
            set
            {
                base.Height = value;
                base.Width = value;
            }
            get { return base.Height; }
        }
    }

我们修改了程序,看上去Square类和Rectangle类都能很好的工作.它们都和数学上的意义保持一致.测试也通过了.

View Code
[Test]
        public void Test()
        {
            Square s = new Square();

            s.SetHeight(10.00);

            Assert.AreEqual(10.00, s.Width);

            s.SetWidth(20.00);

            Assert.AreEqual(20.00, s.Height);
            Assert.AreEqual(s.Width, s.Height);
            Assert.AreEqual(400.00, s.Area());

            Rectangle r = s;

            r.SetHeight(30.00);

            Assert.AreEqual(30.00, s.Width);

            r.SetWidth(40.00);

            Assert.AreEqual(40.00, s.Height);
            Assert.AreEqual(s.Width, s.Height);
            Assert.AreEqual(1600.00, r.Area());
        }

令人意外的是,也许在某个阴暗的角落猥琐着这样一段代码...

View Code
 public void g(Rectangle r)
        {
            r.SetHeight(5.00);
            r.SetWidth(4.00);
            if (r.Area() != 20.00)
            {
                throw new Exception("Bad Area!");
            }
        }

对于Rectangle类型来说,这个函数可以正常运行.但是对于Square类型来说,运行时就会出现异常.所以,真正的问题在于函数g的编写者假设改变长度不会导致其宽度的变化.显然对于长方形来说这个假设是合理的,但是对于正方形来说这就是错误的.

这个问题归结为谁的错误呢?g的编写者认为矩形的原理和不变性说明他写的代码适用于Rectangle类,这种不变性就是长和宽是独立的.但是Square类的编写者也坚持认为他写的代码没有违反正方形的不变性,可是当Square从Rectangle类中派生,Square显然违反了基类的不变性!