Liskov替换原则(C#)
《敏捷软件开发:原则、模式与实践》学习笔记
OCP背后的主要机制是抽象和多态。支持抽象和多态的关键机制是继承。如何是最佳的继承层次?Liskov替换原则描述为:子类型必须能够替换掉它们的基类型。它的替换规则认为:若对类型S的每一个对象o1,都存在一个类型的T的对象o2,使得在所有针对T编写的程序P中,用o1替换o2后,程序的行为功能不变,则S是T的子类型。
下面是一个简单的违反LSP的例子,其中的DrawShape函数违反了OCP(开放-封闭原则),他必须知道Shape类每个可能的派生类,每次每次派生新类时都要修改它。
struct Point{
double x;
double y
};
public enum ShapeType{square,circle};
public class Shape{
private ShapeTypetype;
public Shape(ShapeType t){
type = t;
}
public static void DrawShape(Shape s){
if(s.type == ShapeType.square)
(s as Square).Draw();
if(s.type == ShapeType.circle)
(s as Circle).Draw();
}
}
public class Circle : Shape {
private Pointcenter;
private double radius;
public Circle : base(ShapeType.circle) {}
public void Draw() {/* draws the circle */}
}
public class Square : Shape {
private PointtopLeft;
private double side;
public Circle : base(ShapeType.square) {}
public void Draw() {/* draws the circle */}
}
还有一种更微妙的违反情形,考虑下面的代码
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原则(假设a,b都是一种类型,a is-a b,说明a类对象是b类对象,只不过是特殊的一种。比如说”飞机“是一种“交通工具”。对应到c++中就是继承关系。假设这个程序运行的很好,但某一天,需要添加操作正方形的功能。
a has-a b,说明a类对象具有若干b类对象作为其成员。比如“飞机”有“翅膀”) ,我们就派生一个类Square,继承于Rectangle,这看上去是合理的。
但是我们注意到正方形是不同时需要Width、Height的,这样就造成了浪费,比如在CAD中复杂电路的每个管脚引线都使用正方形进行绘制,需要创建成千上万个正方形,浪费成都很大。
同时正方形也没有Width和Height的概念,如果你不在乎效率的话,可以把程序修正如下能用的地步:
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 Width {
set {
base.Width= value;
base.Height= value;
}
}
}
现在看起来代码似乎可以用了,这个设计看起来是自相容的,正确的,可实际上是错误的。一个自相容的设计未必就和所有的用户程序相容。考虑下面的函数:
void g(Rectangle r)
{
r.Width = 5;
r.Height = 4;
if(r.Area()!= 20)
throw new Exception("Bad Area!");
}
这个函数默认认为传递进来的一定是Rectangle,但如果传进来的是Square,就会抛出异常,因为函数g的编写者默认假设改变Rectangle的宽不会导致其长的改变。
因此LSP让我们得出一个重要的结论:一个模型,如果孤立的看,并不具有真正意义上的有效性。模型的有效性只能通过它的客户端来表现。
那么问题到底发生在哪里呢?是IS-A有问题吗?
因为Square对象的行为方式和函数g所期望的Rectangle对象的行为方式不相容,从行为方式的角度看,Square不是Rectangle,对象的行为方式才是真正的软件所关注的问题。LSP清楚的指出,OOD中IS-A关系是就行为方式而言的。
基于契约设计
基于契约设计(Designby Contract ,DBC)支持了LSP。使用DBC类的编写者显式地规定针对该类的契约。契约为每个方法声明前置条件和后置条件。要使一个方法得以执行,必须使前置为真,且执行完,后置条件为真。
设置Rectangle.Widht的方法的后置条件为:
Assert((width == w) && (height == old.width));
派生类的前置和后置条件规则为:
在重新声明派生类中的方法时,只能使用相等的或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。
本人为初学者,如有不对,还请指教。