Liskov替换原则(C#)

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

派生类的前置和后置条件规则为:

在重新声明派生类中的方法时,只能使用相等的或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。


本人为初学者,如有不对,还请指教。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值