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


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

None.gif class  Rectangle
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif 
protected double width;
InBlock.gif 
protected double height;
InBlock.gif
InBlock.gif 
public double Width
ExpandedSubBlockStart.gifContractedSubBlock.gif 
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif  
setdot.gif{this.width=value;}
ExpandedSubBlockStart.gifContractedSubBlock.gif  
getdot.gif{return this.width;}
ExpandedSubBlockEnd.gif }

InBlock.gif
InBlock.gif 
public double Height
ExpandedSubBlockStart.gifContractedSubBlock.gif 
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif  
setdot.gif{this.height=value;}
ExpandedSubBlockStart.gifContractedSubBlock.gif  
getdot.gif{return this.height;}
ExpandedSubBlockEnd.gif }

InBlock.gif
InBlock.gif 
public double Area//计算长方形的面积
ExpandedSubBlockStart.gifContractedSubBlock.gif
 dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif  
getdot.gif{return this.width*this.height;}
ExpandedSubBlockEnd.gif }

ExpandedBlockEnd.gif}


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

None.gif class  Square : Rectangle
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
public new double Width
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif        
set dot.gifbase.Height = base.Width = value; }
ExpandedSubBlockStart.gifContractedSubBlock.gif        
get dot.gifreturn base.Width; }
ExpandedSubBlockStart.gifContractedSubBlock.gif    }
/**//*由于父类Rectangle在设计时没有考虑将来会被Square继承,所以父类中字段width和height都被设成private,在子类Square中就只能调用父类的属性来set/get。*/
InBlock.gif
InBlock.gif    
public new double Height
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif        
set dot.gifbase.Width = base.Height = value; }
ExpandedSubBlockStart.gifContractedSubBlock.gif        
get dot.gifreturn base.Height; }
ExpandedSubBlockEnd.gif    }

ExpandedBlockEnd.gif}


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

None.gif void  TestRectangle(Rectangle r)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif r.Weight
=10;
InBlock.gif r.Height
=20;
InBlock.gif Assert.AreEqual(
10,r.Weight);
InBlock.gif Assert.AreEqual(
200,r.Area);
ExpandedBlockEnd.gif}

None.gifRectangle r 
=   new  Rectanglt();
None.gifTestRectangle(r);
None.gif

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

None.gif Square s  =   new  Square();
None.gifTestRectangle(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与模式》
《重构》

转载于:https://www.cnblogs.com/happyhippy/archive/2007/05/06/737040.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值