LSP The The Liskov Substitution Principle(里氏代换原则)

 什么是里氏代换原则

    里氏代换原则是由麻省理工学院(MIT)计算机科学实验室的Liskov女士,在1987年的OOPSLA大会上发表的一篇文章《Data Abstraction and Hierarchy》里面提出来的,主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中的蕴涵的原理。2002年,我们前面单一职责原则中提到的软件工程大师Robert C. Martin,出版了一本《Agile Software Development Principles Patterns and Practices》,在文中他把里氏代换原则最终简化为一句话:“Subtypes must be substitutable for their base types”。也就是,子类必须能够替换它的基类.

我们把里氏代换原则解释得更完整一些:在一个软件系统中,子类应该可以替换任何基类能够出现的地方,并且经过替换以后,代码还能正常工作。

这里先回顾下c++中,关于基类和派生类之间的使用兼容关系:

一个派生类的对象在使用上可以被当作基类的对象,反之则禁止。具体表现在:
           -派生类的对象可以赋值给基类对象。
           -派生类的对象可以赋值给基类的引用。
           -派生类对象的地址可以赋值给基类指针。

下面是经典的“正方形不是长方形”-----从正方形和长方形的行为角度来分,不是从数学定义上来分。

class Rectangle
{
public:
    Rectangle() { }

    long GetWidth(){
        return width;
    }
    virtual void SetWidth(long wd){
        width = wd;
    }
    long GetLength(){
        return length;
    }
    virtual void SetLength(long lg){
        length = lg;
    }
protected:
    long width;
    long length;
};

class Square : public Rectangle
{
public:
    void SetWidth(long width){
        this->width = width;
        this->length = width;
    }
    void SetLength(long length){
        this->length = length;
        this->width = length;
    }
};

void Resize(Rectangle* obj_rec)
{
    if (NULL == obj_rec) return;

    while(obj_rec->GetWidth() < obj_rec->GetLength() ) {
        obj_rec->SetWidth( obj_rec->GetWidth() + 1 );
    }
}

   int main()
   {
    //Test code

//Rectangle tec;
 //   tec.SetWidth(6);
 //   tec.SetLength(6); 
    Square square;
    square.SetWidth(6);
    square.SetLength(6);
    Resize(&square);

    }

如果我们测试代码TestFun中传入Rectangle类型参数,则长方形的宽度逐渐增加,直到宽度等于长度时程序结束;

如果我们传入Square类型参数,则正方形的长度和宽度逐渐同时增加,一直死循环下去-----因为长度一直等于宽度。

所以说,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。派生类与基类的行为不一致,因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。

 

    第二个例子:鸵鸟不是鸟
“鸵鸟非鸟”也是一个理解里氏代换原则的经典的例子。“鸵鸟非鸟”的另一个版本是“企鹅非鸟”,这两种说法本质上没有区别,前提条件都是这种鸟不会飞。生物学中对于鸟类的定义:“恒温动物,卵生,全身披有羽毛,身体呈流线形,有角质的喙,眼在头的两侧。前肢退化成翼,后肢有鳞状外皮,有四趾”。所以,从生物学角度来看,鸵鸟肯定是一种鸟。
我们设计一个与鸟有关的系统,鸵鸟类顺理成章地由鸟类派生,鸟类所有的特性和行为都被鸵鸟类继承。大多数的鸟类在人们的印象中都是会飞的,所以,我们给鸟类设计了一个名字为fly的方法,还给出了与飞行相关的一些属性,比如飞行速度(velocity)。织梦好,好织梦

class Bird {
public:
    void fly() { }; //I am flying;
    void setVelocity(int velocity) {velocity = velocity; };
    int getVelocity();
protected:
    int m_velocity;
};
int Bird::getVelocity()
{
    return m_velocity;
}

class Ostrich : public Bird
{
public:
    void fly() {  };//I do nothing;
    void setVelocity(int velocity);//I can't fly,so the speed is zero.
};
void Ostrich::setVelocity(int velocity)
{
    m_velocity = 0;
}

void CalTime(Bird bird)
{
    int time = 0;
    time = 1000/bird.getVelocity();
}

int main()
{
    //Flying time across the river which's width is 1000
    Ostrich ostr;
    ostr.setVelocity(100);
    CalTime(ostr);

如果我们拿一种飞鸟来测试这段代码,没有问题,结果正确,符合我们的预期,系统输出了飞鸟飞越黄河的所需要的时间;如果我们再拿鸵鸟来测试这段代码,结果代码发生了系统除零的异常,明显不符合我们的预期。

 

2  分析原因

    向对象的设计关注的是对象的行为,它是使用行为来对对象进行分类的,只有行为一致的对象才能抽象出一个类来。我经常说类的继承关系就是一种“Is-A”关系,实际上指的是行为上的“Is-A”关系,可以把它描述为“Act-As

 继承关系要求子类要具有基类全部的行为。这里的行为是指落在需求范围内的行为.

 

如何正确地运用里氏代换原则
   
里氏代换原则目的就是要保证继承关系的正确性。我们在实际的项目中,是不是对于每一个继承关系都得费这么大劲去斟酌?不需要,大多数情况下按照“Is-A”去设计继承关系是没有问题的,只有极少的情况下,需要你仔细处理一下,这类情况对于有点开发经验的人,一般都会觉察到,是有规律可循的。

 

通过里氏代换原则给我们带来了什么样的启示?
    类的继承原则:如果一个继承类的对象可能会在基类出现的地方出现运行错误,则该子类不应该从该基类继承,或者说,应该重新设计它们之间的关系。
    
动作正确性保证:符合里氏代换原则的类扩展不会给已有的系统引入新的错误。

 

你可能感兴趣的:(生物,velocity,测试,Class,hierarchy,Types)