第2章 面向对象的设计原则(SOLID):2_里氏替换原则(LSP)

时间:2023-03-08 23:57:20
第2章 面向对象的设计原则(SOLID):2_里氏替换原则(LSP)

2. 里氏替换原则(Liskov Substitution Principle,LSP)

2.1 定义

(1)所有使用基类的地方必须能透明地使用子类替换,而程序的行为没有任何变化(不会产生运行结果错误或异常)。只有这样,父类才能被真正复用,而且子类也能够在父类的基础上增加新的行为。也只有这样才能正确的实现多态

(2)当一个类继承了另一个类时,子类就拥有了父类中可以继承下来的属性和操作。但如果子类覆盖了父类的某些方法,那么原来使用父类的地方就可能会出现错误,因为表面上看,它调用了父类的方法,但实际运行时却调用了被子类覆盖的方法,而这两个方法的实现可能不一样,这就不符合LSP原则。(见后面的解决方案)

(3)里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

【编程实验】正方形与长形的驳论

//1、正方形是一种特殊的长方形(is - a关系)?

#include <stdio.h>

//长方形类
class Rectangle
{
protected:
long width;
long height;
public:
void setWidth(long width){this->width = width;}
long getWidth(){return this->width;} void setHeight(long height){this->height = height;}
long getHeight(){return this->height;} long getArea(){return width * height;}
}; //正方形类(如果继承自长方形类)
class Square : public Rectangle
{
public:
void setWidth(long width)
{
this->width = width;
this->height = width;
} long getWidth(){return this->width;} void setHeight(long height)
{
this->width = height;
this->height = height;
} long getHeight(){return this->height;}
}; int main()
{
//LSP原则:父类出现的地方必须能用子类替换
Rectangle* r = new Rectangle();//Square *r = new Square(); r->setWidth();
r->setHeight(); printf("Area = %d\n",r->getArea()); //当用子类时,结果是16。用户就不
//明白为什么长5,宽4的结果不是20,而是16.
//所以正方形不能代替长方形。即正方形不能
//继承自长方形的子类
return ;
}

//2. 改进的继承关系——符合LSP原则

#include <stdio.h>

//抽象的四方形类
class QuadRangle
{
public:
//将四方形抽象出公共部分出来
virtual long getArea() = ; //面积
virtual long getPerimeter() = ;//周长
}; //长方形类(继承自抽象的四方形类)
class Rectangle : public QuadRangle
{
private:
long width;
long height;
public:
Rectangle(long width, long heigth)
{
this->width = width;
this->height = heigth;
} void setWidth(long width){this->width = width;}
long getWidth(){return this->width;} void setHeight(long height){this->height = height;}
long getHeight(){return this->height;} long getArea(){return width * height;}
long getPerimeter(){return (width + height) * ;}
}; //正方形类(继承自抽象的四方形类)
class Square : public QuadRangle
{
long side;
public:
Square(long side) {this->side = side;} void setSide(long side);
long getSide(){return this->side;}
long getPerimeter(){return * side;}
long getArea(){return side * side;}
}; int main()
{
//LSP原则:父类出现的地方必须能用子类替换
QuadRangle* q = new Rectangle(, ); //Rectangle* q = new Rectangle(5, 4);或Square *q = new Square(5); printf("Area = %d, Perimeter = %d\n",q->getArea(), q->getPerimeter()); return ;
}

【编程实验】鸵鸟不是鸟

//面向对象设计原则:LSP里氏替换原则
//鸵鸟不是鸟的测试程序 #include <stdio.h> //鸟类
class Bird
{
private:
double velocity; //速度
public:
virtual void fly() {printf("I can fly!\n");}
virtual void setVelocity(double v){velocity = v;}
virtual double getVelocity(){return velocity;}
}; //鸵鸟类Ostrich
class Ostrich : public Bird
{
public:
void fly(){printf("I can\'t fly!");}
void setVelocity(double v){Bird::setVelocity();}
double getVelocity(){return Bird::getVelocity();}
}; //测试函数
void calcFlyTime(Bird& bird)
{
try
{
double riverWidth = ; if(bird.getVelocity()==) throw ; printf("Velocity = %f\n", bird.getVelocity());
printf("Fly time = %f\n", riverWidth /bird.getVelocity());
}
catch(int)
{
printf("An error occured!") ;
}
} int main()
{
//遵守LSP原则时,父类对象出现的地方,可用子类替换
Bird b; //用子类Ostrich替换Bird b.setVelocity(); calcFlyTime(b); //父类测试时是正常的,子类时会抛出异常,违反LSP return ;
}

2.2 LSP原则的4层含义

(1)子类必须实现父类中声明的所有方法。

第2章 面向对象的设计原则(SOLID):2_里氏替换原则(LSP)

  ①步枪、手枪和机关枪都继承于AbstractGun,因此都实现了shoot(射击)的功能。

  ②玩具枪不能直接继承于AbstractGun。因为玩具枪不能去实现父类的shoot功能(即子类不能完全实现父类的方法,违反LSP原则),否则这样的武器拿给士兵去杀敌会闹笑话。因此,ToyGun不能继承于AbstractGun,而是继承于AbstracToy,然后去仿真枪的行为。这样对于士兵类来讲,因要求传入的是AbstactGun类的对象,所以不能使用玩具手枪杀人。

(2)子类可以扩展功能,但不能改变父类原有的功能

  ①子类可以有自己的属性和操作。因此,里氏替换原则只能正着用,不能返过来用。即子类出现的地方,父类未必就可以替换。如Snipper类的killEnemy方法中不能传入Rifle类的对象,因为Rifle类中没有zoomOut的方法。

  ②父类向下转换是不安全的,可能会调用到只有在子类中出现的方法而造出异常。

第2章 面向对象的设计原则(SOLID):2_里氏替换原则(LSP)

(3)子类可以实现父类的抽象方法,但一般不要覆盖父类的非抽象方法。

4)如果覆盖或实现父类方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。方法的后置条件(即方法的返回值)要比父类更严格

  ①子类只能使用相等或更宽松的前置条件来替换父类的前置条件。当相等时表示覆盖,不同时表示重载。

  为什么只能放大?因为父类方法的参数类型相对较小,所以当传入父类方法的参数类型(或更窄类型)时,重载时将优先匹配父类的方法,而子类的重载方法不会匹配,因此保证了仍执行父类的方法,所以业务逻辑不变(对于C++而言,父子类之间的同名函数发生隐藏而不是重载,因父类的函数被隐藏,当用子类替换父类时,永远调用不到父类的函数,LSP将无法被遵守)。若是覆盖时,必须清楚其逻辑要义,因为覆盖时子类的方法会被执行)

  ②只能使用相等或更强的后置条件来替换父类的后置条件。即返回值应该是父类返回值的子类或更小

  如果是重载,由于前置条件的要求,会调用到父类的函数,因此子类函数不会被调用

  如果是覆盖,则调用子类的函数,这时子类的返回值(S类型)比父类要求的小(T类型),这是被允许的,因为父类调用函数的时候,返回值至少是T类型,而子类的返回值S(类型小),给T类型的变量赋值是合法的。

  Father F = ClassF.Func();//;用子类替换时Father F = ClassC.Func()是合法的

【编程实验】前置条件和后置条件

#include <stdio.h>

class Shape
{
}; class Rectangle : public Shape
{ }; class Father
{
public:
virtual void drawShape(Shape s) //
{
printf("Father:drawShape(Shape s)\n");
} virtual void showShape(Rectangle r) //
{
printf("Father:ShowShape(Rectangle r)\n");
} Shape CreateShape()
{
Shape s;
printf("Father: Shape CreateShape()");
return s;
}
}; class Son : public Father
{
public: //对于C++而言,重载只能发生在同一作用域。显示Son和Father是不同作用域
//所以,下面发生的是隐藏,而不是重载!因此,当使用子类时,不管下列
//函数中的形参是否比父类更严格,只要同名,父类virtual一律被隐藏。 //子类的形参类型比父类更严格
virtual void drawShape(Rectangle r)
{
printf("Son:drawShape(Rectangle r)\n");
} //子类的形参类型比父类严宽松
virtual void showShape(Shape s)
{
printf("Son:showShape(Shape s)\n");
} //返回值类型比父类严格
Rectangle CreateShape()
{
Rectangle r;
printf("Son: Rectangle CreateShape()"); return r;
}
}; int main()
{
//当遵循LSP原则时,使用父类地方都可以用子类替换 //Father* f = new Father(); //该行可用子类替换
Son* f = new Son(); //用子类替换父类出现的地方 Rectangle r; //子类形参类型更严格时,下一行输出结果会发生变化,不符合LSP原则
f->drawShape(r); //Father类型的f时,调用父类的drawShape(Shape s)
//Son类型的f时,发生隐藏,会匹配子类的drawShape //子类形参类型更宽松时,对于C++而言,会因发生隐藏而不符合LSP原则。但Java发生重载,会符合LSP
f->showShape(r); //Father类型的f时,直接匹配父类的showShape(Rectangle r)
//Son类型的f时,因发生隐藏,会匹配子类的showShape(Shape s) //子类的返回值类型更严格
Shape s = f->CreateShape(); //替换为子类时,返回值为Rectangle,比Shape类型小,这种赋值是合法的 delete f; return ;
}