1
我们把里氏代换原则解释得更完整一些:在一个软件系统中,子类应该可以替换任何基类能够出现的地方,并且经过替换以后,代码还能正常工作。
这里先回顾下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”。
继承关系要求子类要具有基类全部的行为。这里的行为是指落在需求范围内的行为.
3
通过里氏代换原则给我们带来了什么样的启示?