六大设计原则(二)LSP里氏替换原则

里氏替换原则LSP(Liskov Subsituation Principle)

里氏替换原则定义

所有父类出现的地方可以使用子类替换并不会出现错误或异常,但是反之子类出现的地方不一定能用父类替换。

LSP的四层含义

  • 子类必须完全实现父类的方法
  • 子类可以自己的个性(属性和方法)
  • 覆盖或实现父类的方法时输入参数可以被放大
  • 覆盖或实现父类的方法时输出结果可以被缩小

LSP的定义含义1——子类必须完全实现父类的方法

假设如下场景:定义一个枪支抽象类,一个场景类,三个枪支实现类,一个士兵类。此处,三个枪支完全实现了父类的方法。

关联关系:实线箭头
泛化关系:实线空心箭头(继承关系)
依赖关系:虚线箭头(使用关系)一个类需要另一个类的协助


抽象枪支类:射击功能

package des.lsp;

/**
 * 抽象类 枪支
 */
abstract class AbstractGun {
    //射击功能
  public abstract void shoot();
}

子类实现

package des.lsp;

/**
 * 手枪
 */
public class HandGun extends AbstractGun {
    @Override
    public void shoot() {
        System.out.print("手枪可以射击");
    }
}
package des.lsp;

/**
 * 手枪
 */
public class MachineGun extends AbstractGun {
    @Override
    public void shoot() {
        System.out.print("步枪可以射击");
    }
}
package des.lsp;

/**
 * 步枪
 */
public class Rifle extends AbstractGun {
    @Override
    public void shoot() {
        System.out.print("步枪可以射击");
    }
}

士兵类:士兵类使用的是抽象枪支类,具体的需要在场景类中指定。

类中调用其他类必须使用父类或接口,若不能使用则其实已经违背了LSP原则。

package des.lsp;

public class Soldier {
    private AbstractGun gun;
    public void setGun(AbstractGun _gun){
        this.gun = _gun;
    };
    public void killEnemy(){
        System.out.print("士兵开始杀人...");
        gun.shoot();
    }

}

场景类

package des.lsp;

public class Client {
    public static void main(String[] args) {
        // write your code here
        Soldier s = new Soldier();
        s.setGun(new Rifle());
        s.killEnemy();
    }
}

如果加入一个玩具枪类,即玩具枪类同样继承抽象枪支类,此时就会存在子类不能实现枪支类方法的情况,因为玩具枪和枪最本质的区别是玩具枪不能射击的,是无法杀死人的。但是,玩具枪的其他属性,比如颜色等一些属性可以委托抽象枪支类进行处理。

如果子类不能完整的实现父类的方法或者父类某些方法在子类中发生了畸变,则应该断开父子关系采用依赖、组合、聚集等关系来代替原有的继承。

玩具枪继承枪支抽象类的情况:射击方法不能被实现,如果实现里面具体逻辑为空则毫无意义,即正常情况下不能实现父类的shoot方法,shoot方法必须去掉,从LSP来看如果去掉,则违背了LSP的第一个原则:子类必须实现父类方法。(代码层面来看如果去掉则会报错)

package des.lsp;

public class ToyGun extends  AbstractGun {
    @Override
    public void shoot() {
        //此方法不能实现,玩具枪不能射击
    }
}

解决方法:单独建立一个抽象类玩具类,把与枪支共有的如声音、颜色交给抽象枪支类处理,而玩具枪所特有的玩具类的属性交给抽象玩具类处理,玩具枪类实现玩具抽象类

LSP的定义含义2——子类可以含有自己的特性

如图引入,步枪的实现类即步枪由不同的型号。AUG:狙击枪可以由望远镜功能zoomOut方法。


此处Snipper是狙击手类,狙击手与狙击枪是密不可分,属于组合关系,所以狙击手类直接使用子类AUG。

package des.lsp;
//狙击枪
public class AUG extends Rifle {
    //狙击枪特有功能
    public void zoomOut(){
        System.out.print("通过望远镜观察敌人...");
    }

    @Override
    public void shoot() {
        System.out.print("AUG射击敌人...");
    }
}
package des.lsp;
//狙击手
public class Snipper {
    //此处传入参数为子类,组合关系
    public void killEnemy(AUG aug){
        //观察
        aug.zoomOut();
        //射击
        aug.shoot();
    }
}
package des.lsp;

public class Client {
    public static void main(String[] args) {

        Snipper s = new Snipper();
        s.killEnemy(new AUG());
    }
}

LSP原则:父类不一定能替换子类

package des.lsp;

public class Client {
    public static void main(String[] args) {
        // write your code here
//        Soldier s = new Soldier();
//        s.setGun(new Rifle());
//        s.killEnemy();

        Snipper s = new Snipper();
        s.killEnemy((AUG) new Rifle());//此处用父类代替了子类
    }
}

报错代码

LSP的定义含义3——覆盖或实现父类方法时输入参数可以被放大

假设有如下场景
父类:方法入参<子类方法入参

场景类调用:父类调用自己方法。

package des.lsp;

import java.util.HashMap;

public class Client {
    public static void invoker(){
        Father f = new Father();
        HashMap map = new HashMap();
        f.doSomething(map);

    }
    public static void main(String[] args) {

        invoker();
    }
}

输出结果

使用里氏替换原则:把所有父类出现的地方替换为子类

package des.lsp;

import java.util.HashMap;

public class Client {
    public static void invoker(){
        Son f = new Son();
        HashMap map = new HashMap();
        f.doSomething(map);

    }
    public static void main(String[] args) {
emy((AUG) new Rifle());

        invoker();
    }
}

输出结果

我们的本意是调用子类重载的方法,入参为Map的方法,但实际程序执行是调用的从父类继承的方法。如果子类的方法中入参的范围大于父类入参的范围,则子类代替父类的时候,子类的方法永远不会执行。
从另外角度来看,假如父类入参的范围大于子类的入参的范围,则父类替换子类就未必能存在,这时候很可能会调用子类的方法执行。此句话较为抽象,实际情况如下。

父类和子类的代码如下

public class Father {
    public Collection doSomething(Map map){
        System.out.print("父类被执行...");
        return map.values();
    }
}
public class Son extends Father {
    public Collection doSomething(HashMap map) {
       System.out.print("子类执行...");
        return map.values();
    }
}

场景类:调用父类

package des.lsp;

import java.util.HashMap;

public class Client {
    public static void invoker(){
        Father f = new Father();
        HashMap map = new HashMap();
        f.doSomething(map);

    }
    public static void main(String[] args) {

        invoker();
    }
}

运行结果:不言而喻,是父类被执行

采用LSP后

package des.lsp;

import java.util.HashMap;

public class Client {
    public static void invoker(){
        Son f = new Son();
        HashMap map = new HashMap();
        f.doSomething(map);

    }
    public static void main(String[] args) {

        invoker();
    }
}


此时一般人会想,难道不是子类执行吗?因为子类的入参就是HashMap,肯定要调用这个。
但是此时要考虑一个问题,假如我们的本来意思是就是调用从父类继承的入参为Map的方法,但是程序执行的时候却自动为我们执行了子类的方法,此时就会导致混乱。
结论:子类中的方法的输入参数(前置条件或称形式参数)必须与父类中的输入参数一致或者更宽松(范围更大)。

LSP的定义含义4——覆盖或实现父类的方法时输出结果可以被缩小

理解:父类的返回类型为T,子类的返回类型为S,即LSP要求S<= T
此时分为两种情况

  • 如果时覆写,子类继承父类,继承的方法的入参必然相同,此时传入参数必须时相同或小于,返回的值必然不能大于父类返回值,这是覆写的要求。
  • 如果时重载,这时候要求子类重载方法的参数类型或数量不相同,其实就是保证输入参数宽于或等于父类输入参数,这时候就保证了子类的方法永远不会被执行,其实就是含义3。

LSP的目的及理解

  • 增强程序的健壮性
  • 保证即使增加子类,原有的子类仍然可以继续运行。
  • 从一方面来说,在程序中尽量避免直接使用子类的个性,而是通过父类一步一步的使用子类,否则直接使用子类其实就相当于直接把子类当作父类,这就直接导致父类毫无用途,父类和子类的关系也会显得没有必要存在了。

原文地址:https://www.cnblogs.com/quinntian/p/10739178.html

时间: 2024-11-05 13:33:53

六大设计原则(二)LSP里氏替换原则的相关文章

设计原则(二)里氏替换原则(LSP)

一.什么是里氏替换原则 里氏替换原则的严格表达是: 如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型. 换言之,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别. 比如,假设有两个类,一个是Base类,另一个是Child类,并且Child类是Base的子类.那么一个方法如果可以接受一个基类对象b的话:method1(Ba

面向对象设计原则三:里氏替换原则(LSP)

里氏替换原则(LSP)定义:在任何父类出现的地方都可以用它的子类类替换,且不影响功能.解释说明:其实LSP是对开闭原则的一个扩展,在OO思想中,我们知道对象是由一系列的状态和行为组成的,里氏替换原则说的就是在一个继承体系中,对象应该具有共同的外在特性,使用LSP时,如果想让我们的程序达到一个父类出现的地方都可以用它的子类来替换且不影响功能,那么这个父类也应该尽量声明出子类所需要的一些公共的方法,父类被子类替换之后,会比较顺利,那么为什么说它是对开闭原则的一个扩展呢?因为我们在开闭原则中说尽量使用

设计模式六大原则之二:里氏替换原则

定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类A完成.现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成.新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障. 解决方

设计模式六大原则: 狸猫换太子 -- 里氏替换原则

据野史传说中记载,当年宋哲宗最宠爱的妃子是刘德妃.刘德妃虽然深受皇上宠爱,但是却久久不能生育.刘德妃为竞争皇后之位,提高自己的身价,便想出了"借腹怀孕"的诡计.她打算利用身旁的一个长得有些姿色的姓李的侍女,引诱宋真宗上钩.一次在刘德妃沐浴之时,真宗果真临幸了这个李姓侍女,不久这个侍女就怀孕了,这使刘德妃也装作怀孕的样子,其实侍女怀孕是真,刘德妃只是安排计策假装而已.等到十月分娩的时候,"两个"龙种先后呱呱落地.刘德妃采取了狸猫换太子的卑劣手段,让真宗将李姓侍女打入冷

[转]设计模式六大原则[2]:里氏替换原则

肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类A完成.现需

面向对象五大原则_1.单一职责原则&amp;amp;2.里氏替换原则

单一职责原则:Single Responsibility Principle (SRP) 一个类.仅仅有一个引起它变化的原因.应该仅仅有一个职责.每个职责都是变化的一个轴线.假设一个类有一个以上的职责,这些职责就耦合在了一起.这会导致脆弱的设计.当一个职责发生变化时,可能会影响其他的职责.另外,多个职责耦合在一起,会影响复用性. 比如:要实现逻辑和界面的分离. T负责两个不同的职责:职责P1.职责P2.当因为职责P1需求发生改变而须要改动类T时.有可能会导致原本执行正常的职责P2功能发生问题.

七、LSP 里氏替换原则

子类的对象提供了父类的所有行为,且加上子类额外的一些东西(可以是功能,可以是属性).当程序基于父类实现时,如果将子类替换父类而程序不需修改,则说明符合LSP原则. 这个解释看的似懂非懂,再看下面更进一步的解释: 函数使用指向父类的指针或引用时,必须能够在不知道子类类型的情况下使用子类的对象. 子类必须能够替换成它们的父类 这其中存在这样的概念:方法调用者(C)和方法提供者(P).C调用P提供的方法,P的方法返回给C处理的结果.中间的过程C是不需要知道,也不会知道的.当C调用P的方法时,如果将P替

设计模式六大原则(2)--里氏替换原则

定义: 程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换,也就是说所有引用基类的地方必须能透明地使用其子类的对象.通俗的来说,子类可以扩展父类的功能,但不能改变父类原有的功能. 由来: 第一次看见这个里氏替换原则的名字会觉着很奇特,根据以往的经验这一看就是外国友人首先提出的概念,然后便以她的姓命名该原则.确实是这样,它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出.里氏替换原则英文名称为Liskov Substi

Java设计原则—里氏替换原则(转)

里氏替换原则(Liskov Substitution Principel)是解决继承带来的问题. 继承的优点: 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性: 提高代码的重用性: 子类可以形似父类,但又异于父类: 提高代码的可扩展性: 提高产品或项目的开放性. 继承的缺点: 继承是侵入性的,只要继承就必须拥有父类的所有属性和方法: 降低代码的灵活性,子类必须拥有父类的属性和方法,让子类增加了约束: 增强了耦合性,当父类的常量.变量和方法被修改时,必须考虑子类的修改. 定义: 所有