第3章 里氏替换原则(LSP)

前端之家收集整理的这篇文章主要介绍了第3章 里氏替换原则(LSP)前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

一、定义

(1)、所有使用基类的地方必须能够使用子类进行替换,而程序的行为不会发生任何变化(替换为子类之后不会产生错误或者异常)。

只有这样,父类才能真正被复用,子类能够在父类的基础上增减新的属性和行为。才能真正的实现多态行为。

(2)、当子类继承父类的时候,子类就拥有了父类属性和行为。(注意:只是类型而已) 但是如果子类覆盖父类的某些方法,那么

来使用父类的地方就可能出现错误。(如何理解呢?表面上看是调用的是父类方法,实际运行的时候子类方法覆盖了父类的方

法,注父类方法其实是存在的,通过作用域限定符可以访问到,两个方法的实现可能不一样,这样不符合LSP里氏替换原则。)

(3)、里氏替换原则是实现开闭原则的重要方式之一。由于使用基类对象的地方可以使用子类对象,因此程序中尽量使用基类类型进

定义,而在运行的时候确定子类类型,子类对象替换父类对象。 (有点面向接口编程的味道,对外提供接口,而不是实现类)。

或者可以实现公共父类(父类中公共属性和行为)。

编程实验:长方形和正方形的驳论

1、正方形是一种特殊的长方形(is-a关系):类图:

正方形类继承于长方形类。

int main()
{
    //LSP原则:父类出现的地方必须能用子类替换
    Rectangle* r = new Rectangle();//Square *r = new Square();
    
    r->setWidth(5);
    r->setHeight(4);
    
    printf("Area = %d\n",r->getArea()); //当用子类时,结果是16。用户就不
                                       //明白为什么长5,宽4的结果不是20,而是16.
                                       //所以正方形不能代替长方形。即正方形不能
                                       //继承自长方形的子类
    return 0;
}
2、改进的继承关系---符合LSP原则(面向接口编程)

类图:


int main()
{
    //LSP原则:父类出现的地方必须能用子类替换
    QuadRangle* q = new Rectangle(5,4); //Rectangle* q = new Rectangle(5,4);或Square *q = new Square(5);
       
    printf("Area = %d,Perimeter = %d\n",q->getArea(),q->getPerimeter()); 
    
    return 0;
}

3、鸵鸟不是鸟类
//面向对象设计原则: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(0);}
    double getVelocity(){return Bird::getVelocity();}
};

//测试函数
void calcFlyTime(Bird& bird)  //参数是引用   父类引用子类的时候,会有多态的行为
{
    try
    {
        double riverWidth = 3000;
            
        if(bird.getVelocity()==0) throw 0;
            
        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(100);  //替换之后,会直接调用子类的方法
    
    calcFlyTime(b); //父类测试时是正常的,子类时会抛出异常,违反LSP
    
    return 0;
}
这种小程序都比较简单,我也是看的别人的,主要是用来测试历史替换原则。

二、历史替换原则的4层含义(良好的继承定义规范,主要包括4层含义)

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

java里面的接口可以直接定

义接口对象。

(1)、步枪、手枪和机关枪都继承于AbstractGun接口类,都必须实现shoot(射击)的功能

(2)、玩具枪不能直接继承AbstractGun。因为玩具枪不能实现父类的shoot功能(即子类不能完全实现父类方法,违反LSP原则)。

按照继承原则,上面的玩具枪继承AbstractGun是没有问题的,玩具枪也是枪,但是在具体的应用场景中就要考虑这个问题了:子类

是否能够完整的实现父类的业务,否则就会出现拿枪杀敌人时是把玩具枪的笑话。

因此,ToyGun不能继承于AbstractGun,而是继承AbstracToy,然后仿真枪的行为。因为士兵类要求传入的参数AbstractGun类的对

象,所以不能使用玩具枪杀人。

感觉用C++表示这种关系比较牵强。

(3)、如果子类不能完整的实现父类方法,或者父类的某些方法在子类中已经发生"畸变",则建议断开父子继承关系,采用依赖、

聚合、组合等关系代替继承。

2、子类可以扩展功能,但不能改变父类原有的功能(理解:不能出现方法覆盖的情况,多态可以)

(1)、子类可以有自己的属性方法。因此,里氏替换原则只能正着用,父类出现的地方可以用子类替换,但是不能反过来用。即子

类出现的地方,父类未必可以替换。例如:Snipper类的killEnemy方法中不能传入Rifle类的对象,因为父类中没有子类的zoomOut

方法


(2)、父类向下转换是不安全的,可能会调用只有在子类中出现的方法造成异常。

java里面的接口其实就是C++里面的抽象类,而java里面的抽象类其实就是C++里面的普通的父类(可以有成员变量和方法)。

多继承的实现:单继承+多接口

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

注意:父类抽象方法(多态),一般不要覆盖非抽象方法(子类中公有的父类成分)

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

(1)、子类只能使用相等或者更宽松(表示使用的是父类类型)的前置条件来替换父类的前置条件。相等时表示覆盖,不同时表示的是重载(java中)。

为什么是放大?因为父类方法的参数类型相对较小,所以当传入父类方法的参数类型,重载的时候优先匹配父类方法,而子类的重载方法不会匹

配,因此仍保证执行父类方法(子类继承的时候其实操作的是子类中的父类成分),所以业务逻辑不会改变(C++中,父子类的同名函数发生隐藏而不

是重载,因为父类函数被隐藏,当用子类替换父类时,永远不会调用父类函数,LSP将无法遵守)。若是覆盖时,子类的方法会被执行

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

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

如果是覆盖,则调用子类的函数,这时子类的返回值比父类要求的小。因为父类调用函数的时候,返回值的类型是父类的类型,而子类的返回值更小,

赋值合法。

Father F = ClassF.Func();//;用子类替换时Father F = ClassC.Func()是合法的 子类赋值父类转是合法的,父类赋值给子类是不合法

利用设计模式之禅上面的例子更能详细的说明这点:

实验:(实验也是网上的,但是能用设计模式之禅上面的例子更好)

#include <iostream>

using namespace std;

//定义两个空类型用于实验
class Shape
{
};

class Rectangle : public Shape
{

};
//C++中的抽象类就相当于java中的接口实现
//C++中普通的父类(带有虚函数的,抽象方法)相当于java中的抽象类
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一律被隐藏。

	//子类的形参类型比父类更严格,
	void drawShape(Rectangle r)
	{
		printf("Son:drawShape(Rectangle r)\n");
	}

	//子类的形参类型比父类严宽松:表示的是父类
	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;
	cin.get();
	return 0;
}

猜你在找的设计模式相关文章