热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

2.里氏替换原则(LiskovSubstitutionPrinciple,LSP)2.1定义(1)所有使用基类的地方必须能透明地使用子类替换,而程序的行为没有任何变化(不会产生运行

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

2.1 定义

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

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

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

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

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

#include 

//长方形类
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(5);
    r->setHeight(4);
    
    printf("Area = %d\n",r->getArea()); //当用子类时,结果是16。用户就不
                                       //明白为什么长5,宽4的结果不是20,而是16.
                                       //所以正方形不能代替长方形。即正方形不能
                                       //继承自长方形的子类
    return 0;
}

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

#include 

//抽象的四方形类
class QuadRangle
{   
public:
    //将四方形抽象出公共部分出来
    virtual long getArea() = 0;     //面积
    virtual long getPerimeter() = 0;//周长
};

//长方形类(继承自抽象的四方形类)
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) * 2;}
};

//正方形类(继承自抽象的四方形类)
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 4 * side;}
    long getArea(){return side * side;}
};

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;
}

 【编程实验】鸵鸟不是鸟

//面向对象设计原则:LSP里氏替换原则
//鸵鸟不是鸟的测试程序

#include 

//鸟类
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;
}

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 

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 0;
}

推荐阅读
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • Linux内核那些事之连接跟踪
    “本文分析了Linux内核连接跟踪的关键实现”连接跟踪(也叫会话管理)是状态防火墙关键核心,也是很多网元设备必不可少的一部分。各厂商的实 ... [详细]
  • Java实战之电影在线观看系统的实现
    本文介绍了Java实战之电影在线观看系统的实现过程。首先对项目进行了简述,然后展示了系统的效果图。接着介绍了系统的核心代码,包括后台用户管理控制器、电影管理控制器和前台电影控制器。最后对项目的环境配置和使用的技术进行了说明,包括JSP、Spring、SpringMVC、MyBatis、html、css、JavaScript、JQuery、Ajax、layui和maven等。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 本文介绍了UVALive6575题目Odd and Even Zeroes的解法,使用了数位dp和找规律的方法。阶乘的定义和性质被介绍,并给出了一些例子。其中,部分阶乘的尾零个数为奇数,部分为偶数。 ... [详细]
  • 本文介绍了指针的概念以及在函数调用时使用指针作为参数的情况。指针存放的是变量的地址,通过指针可以修改指针所指的变量的值。然而,如果想要修改指针的指向,就需要使用指针的引用。文章还通过一个简单的示例代码解释了指针的引用的使用方法,并思考了在修改指针的指向后,取指针的输出结果。 ... [详细]
author-avatar
玩上加瘾_926
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有