从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”

假设我们现在的需求是实现一个长方形,于是我们写下了这样的代码:

从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”class Rectangle
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”
{
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” 
protected double width;
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” 
protected double height;
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” 
public double Width
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” 
{
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”  
set{this.width=value;}
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”  
get{return this.width;}
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” }

从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” 
public double Height
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” 
{
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”  
set{this.height=value;}
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”  
get{return this.height;}
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” }

从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” 
public double Area//计算长方形的面积
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”
 {
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”  
get{return this.width*this.height;}
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” }

从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”}


    这个程序运行得很好,并被安装到多个Client。但现在需求增加了,Client需要一个正方体Square。按照平面几何学的观点,正方形是长与宽相等的特殊的长方形,即Square IS-A Rectangle,于是我们让Square继承Rectangle类。(PS:Square继承了Rectangle后它也有了相应的width和height字段,鉴于Square的长和宽相等,它仅需要这两个字段中的一个就够了,这就浪费了内存资源(如果在程序中定义很多个Square对象实例的话)。)

从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”class Square : Rectangle
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”
{
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”    
public new double Width
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”    
{
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”        
set base.Height = base.Width = value; }
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”        
get return base.Width; }
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”    }
/*由于父类Rectangle在设计时没有考虑将来会被Square继承,所以父类中字段width和height都被设成private,在子类Square中就只能调用父类的属性来set/get。*/
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”    
public new double Height
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”    
{
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”        
set base.Width = base.Height = value; }
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”        
get return base.Height; }
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”    }

从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”}


    这段代码貌似运行良好,无论我们对Square和Rectangle对象做任何操作,都与数学上的正方形和长方形保持一致。这样看来设计似乎时自相容的、正确的;但是一个自相容的设计未必与所有的用户程序相容。例如假设我们在定义Square之前,对Rentangle进行了如下的单元测试:

从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”void TestRectangle(Rectangle r)
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”
{
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” r.Weight
=10;
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” r.Height
=20;
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” Assert.AreEqual(
10,r.Weight);
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形” Assert.AreEqual(
200,r.Area);
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”}

从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”Rectangle r 
= new Rectanglt();
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”TestRectangle(r);
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”

    这段测试代码运行OK,但现在我们有了Square类,Square IS-A Rectangle,如果我们传入一个Square对象会如何呢?

从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”Square s = new Square();
从“Liskov替换原则”和“Refused Bequest”看“正方形为什么不能继承长方形”TestRectangle(s);

    现在两个Assert测试都失败了...这样看来,Square在某些场合是不能替代Rectangle的,让Square继承Rectangle是一种不合理的设计,其违背了Liskov替换原则(LSP)。

    (Form《敏捷软件开发:原则、模式与实践》,以下简称PPP)LSP让我们得出一个非常重要的结论:一个模型,如果孤立地看,并不具有真正意义上的有效性,模型的有效性只能通过它的客户程序来表现。例如孤立地看Rectangle和Squre,它们时自相容的、有效的;但从对基类Rectangle做了合理假设的客户程序TestRectangle(Rectangle r)看,这个模型就有问题了。在考虑一个特定设计是否恰当时,不能完全孤立地来看这个解决方案,必须要根据该设计的使用者所作出的合理假设来审视它。

    目前也有一些技术可以支持我们将合理假设明确化,例如测试驱动开发(Test-Driven Development,TDD)和基于契约设计(Design by Contract,DBC)。但是有谁知道设计的使用者会作出什么样的合理假设呢?大多数这样的假设都很难预料。如果我们预测所有的假设的话,我们设计的系统可能也会充满不必要的复杂性。PPP一书中推荐的做法是:只预测那些最明显的违反LSP的情况,而推迟对所有其他假设的预测,直到出现相关的脆弱性的臭味(Bad Smell)时,才去处理它们。我觉得这句话还不够直白,Martin Fowler的《Refactoring》一书中“Refused Bequest”(拒收的遗赠)描述的更详尽:子类继承父类的methods和data,但子类仅仅只需要父类的部分Methods或data,而不是全部methods和data;当这种情况出现时,就意味这我们的继承体系出现了问题。例如上面的Rectangle和Square,Square本身长和宽相等,几何学中用边长来表示边,而Rectangle长和宽之分,直观地看,Square已经Refused了Rectangle的Bequest,让Square继承Rectangle是一个不合理的设计。

    现在再回到面向对象的基本概念上,子类继承父类表达的是一种IS-A关系,IS-A关系这种用法被认为是面向对象分析(OOA)基本技术之一。但正方形的的确确是一个长方形啊,难道它们之间不存在IS-A关系?关于这一点,《Java与模式》一书中的解释是:我们设计继承体系时,子类应该是可替代的父类的,是可替代关系,而不仅仅是IS-A的关系;而PPP一书中的解释是:从行为方式的角度来看,Square不是Rectangle,对象的行为方式才是软件真正所关注的问题;LSP清楚地指出,OOD中IS-A关系时就行为方式而言的,客户程序是可以对行为方式进行合理假设的。其实二者表达的是同一个意思。

 

参考:
《敏捷软件开发:原则、模式与实践》
《Java与模式》
《重构》


本文转自Silent Void博客园博客,原文链接:http://www.cnblogs.com/happyhippy/archive/2007/05/06/737040.html,如需转载请自行联系原作者

上一篇:链路追踪产品商业化


下一篇:django1.6.x(python3.3)使用pymysql连接mysql