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的子类型.
按照这个原则我们可以大致推导出这样一段代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{
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).用户可以设置它的长度和宽度,可以设置它的左上点的坐标.并且会有其他的类能够绘制它们.简单的代码应该是这样
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{
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 { get; set; }
public int Y { get; set; }
}
客户总是多变的.某天他们需要能够绘制正方形 (Square).从一般意义上来说,正方形就是一个特殊的矩形.对吧,初中老师都是这么说的.表面上看,Square Is-A Rectangle,这种关系代表这继承.Square派生自Rectangle.目前来说,这种扩展是没有问题的.可是....编程时我们却发现,Square类的长度和宽度都应该是一样的.但是从Rectangle继承而来的属性却无法满足这个条件.这是存在问题的重要标志,不过我们可以用其他途径解决,比如说这样:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{
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.下面这个测试可以保证是这样:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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);
}
}
不过可能恐怖的事情远比我们想的还要多...看看这下面这段代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{
r.SetWidth(15.80);
}
如果我们现在向这个函数传递一个指向Square对象的引用,这个Square对象就会遭到破坏,因为它的长度肯定不会改变.这显然违反了LSP.或许你可以将问题归咎于在Rectangle类中一些属性和方法没有设置virtual,然而如果派生类的创建导致我们修改基类,这常常意味着设计是有缺陷的,当然也违反了OCP.可是真的设置为virtual就能避免再次发生类似的问题了吗?
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{
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;
}
}
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{
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类都能很好的工作.它们都和数学上的意义保持一致.测试也通过了.
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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());
}
令人意外的是,也许在某个阴暗的角落猥琐着这样一段代码...
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{
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显然违反了基类的不变性!