本帖最后由 LiuKang 于 2013-11-19 21:17 编辑
定义:如果对于类型S的每个对象O1存在类型T的对象O2,那么对于所有定义了T的程序P来说,当用O1替换O2并且S是T的子类型时,P的行为不会改变。子类型能够完全替换父类型,而不会让调用父类型的客户程序从行为上有任何改变。
例:
白马、黑马:
反过来的代换不成立《墨子·小取》说:"娣,美人也,爱娣,非爱美人也……"娣便是妹妹,哥哥喜爱妹妹,是因为两人是兄妹关系,而不是因为妹妹是个美人。因此,喜爱妹妹不等同于喜爱美人。用面向对象语言描述,美人是基类,妹妹是美人的子类。哥哥作为一个有"喜爱()"方法,接受妹妹作为参数。那么,这个"喜爱()"方法一般不能接受美人的实例。
一个违反LSP的简单例子(长方形和正方形)
public class Rectangle{ private long width; private long height; public void setWidth(long width) { this.width = width; } public long getWidth() { return this.width; } public void setHeight(long height) { this.height = height; } public long getHeight() { return this.height; }}
public class Square{ private long side; public void setSide(long side) { this.side = side; }
public long getSide() { return side; }}
正方形不可以做长方形的子类
using System;
public class Rectangle{ private long width; private long height; public void setWidth(long width) { this.width = width; } public long getWidth() { return this.width; } public void setHeight(long height) { this.height = height; } public long getHeight() { return this.height; }}
public class Square : Rectangle{ private long side;
public void setWidth(long width) { setSide(width); }
public long getWidth() { return getSide(); }
public void setHeight(long height) { setSide(height); }
public long getHeight() { return getSide(); }
public long getSide() { return side; }
public void setSide(long side) { this.side = side; }}
public class SmartTest{ public void resize(Rectangle r) { while (r.getHeight() >= r.getWidth() ) { r.setWidth(r.getWidth() + 1); } }} 在执行SmartTest的resize方法时,如果传入的是长方形对象,当高度大于宽度时,会自动增加宽度直到超出高度。但是如果传入的是正方形对象,则会陷入死循环。
代码重构
public interface Quadrangle{ public long getWidth(); public long getHeight();}
public class Rectangle : Quadrangle { private long width; private long height; public void setWidth(long width) { this.width = width; } public long getWidth() { return this.width; } public void setHeight(long height) { this.height = height; } public long getHeight() { return this.height; }}
public class Square : Quadrangle { private long side;
public void setSide(long side) { this.side = side; }
public long getSide() { return side; }
public long getWidth() { return getSide(); }
public long getHeight() { return getSide(); }}
Liskov替换原则 基于这样的一个事实,那就是客户程序在调用某一个类时,实际上是对该类的整个继承体系设定了一个契约,继承体系中的所有类必须遵循这一契约,即前置条件和后置条件必须保持一致 。这就超越了继承中子类与父类之间形成的is-a关系,为对象继承加上了一把严格的枷锁。显然,Liskov替换原则对于约束继承的泛滥具有重要意义。
我个人认为,Liskov替换原则并不是要求子类不能新增父类没有的方法或者属性。因为从调用父类的客户程序的角度来说,它关心的仅仅是父类的行为,只要子类对于父类的行为是可替换的,就不算是违背该原则。恰恰相反,当你发现父类拥有子类不希望继承,或者勉强继承会对子类造成破坏时 ,正可以说明这个继承体系可能存在问题,违背了Liskov替换原则。这就充分说明,子类并不关心父类的行为,但却需要遵循父类制定的规范或契约,以满足客户调用父类的期望。正所谓"萧规曹随",如果前人制定的规范我们不遵循,反而要去打破,那就不是继承,而是铁了心要另起炉灶了。
一个经典的违反Liskov替换原则的例子是正方形与矩形之间的关系。这样的例子在谈对象设计的原则时,已经啰嗦得够多,这里我就不再赘述了 。这个例子带来的教训就是,现实世界中继承的例子,不能够完全直接套用在程序世界中。不过,作为设计的参照物,现实世界的很多规律与法则,我们仍然不可忽视。例如鲸鱼和鱼,应该属于什么关系?从生物学的角度看,鲸鱼应该属于哺乳动物,而不是鱼类。没错,在程序世界中我们可以得出同样的结论。如果让鲸鱼类去继承鱼类,就完全违背了Liskov替换原则。因为鱼作为父类,很多特性是鲸鱼所不具备的,例如通过腮呼吸,以及卵生繁殖。那么,二者是否具有共性呢?有,那就是它们都可以在水中"游泳",从程序设计的角度来说,它们都共同实现了一个支持"游泳"行为的接口。
如图2-7所示的设计,可以看做是解决违背Liskov替换原则的一种常规方案,即提取两者之间的共同点,定义一个更为通用的接口,或者新的父类。
图2-7 鲸鱼和鱼的关系 |
组图打开中,请稍候......
|