happyhippy

这个世界的问题在于聪明人充满疑惑,而傻子们坚信不疑。--罗素


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

class Rectangle
{
 
protected double width;
 
protected double height;

 
public double Width
 
{
  
set{this.width=value;}
  
get{return this.width;}
 }


 
public double Height
 
{
  
set{this.height=value;}
  
get{return this.height;}
 }


 
public double Area//计算长方形的面积
 {
  
get{return this.width*this.height;}
 }

}


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

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

    
public new double Height
    
{
        
set base.Width = base.Height = value; }
        
get return base.Height; }
    }

}


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

void TestRectangle(Rectangle r)
{
 r.Weight
=10;
 r.Height
=20;
 Assert.AreEqual(
10,r.Weight);
 Assert.AreEqual(
200,r.Area);
}

Rectangle r 
= new Rectanglt();
TestRectangle(r);

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

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

posted on 2007-05-06 12:58  Silent Void  阅读(2458)  评论(10编辑  收藏  举报