纯净、安全、绿色的下载网站

首页|软件分类|下载排行|最新软件|IT学院

当前位置:首页IT学院IT技术

Java 设计模式 里氏替换原则 解析Java实现设计模式六大原则之里氏替换原则

盛开的太阳   2021-06-23 我要评论
想了解解析Java实现设计模式六大原则之里氏替换原则的相关内容吗盛开的太阳在本文为您仔细讲解Java 设计模式 里氏替换原则的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Java,设计模式,Java,里氏替换原则下面大家一起来学习吧。

一、什么是里氏替换原则

1.1、里氏替换原则定义

里氏替换原则(Liskov Substitution principle)是对子类型的特别定义的. 为什么叫里氏替换原则呢?因为这项原则最早是在1988年由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的。

里氏替换原则有两层定义:

定义1

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。
如果S是T的子类则T的对象可以替换为S的对象而不会破坏程序。

定义2:

Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。
所有引用其父类对象方法的地方都可以透明的替换为其子类对象

这两种定义方式其实都是一个意思即:应用程序中任何父类对象出现的地方我们都可以用其子类的对象来替换并且可以保证原有程序的逻辑行为和正确性。

1.2、里氏替换原则有至少有两种含义

1.里氏替换原则是针对继承而言的如果继承是为了实现代码重用也就是为了共享方法那么共享的父类方法就应该保持不变不能被子类重新定义。子类只能通过新添加方法来扩展功能父类和子类都可以实例化而子类继承的方法和父类是一样的父类调用方法的地方子类也可以调用同一个继承得来的逻辑和父类一致的方法这时用子类对象将父类对象替换掉时当然逻辑一致相安无事。

2.如果继承的目的是为了多态而多态的前提就是子类覆盖并重新定义父类的方法为了符合LSP我们应该将父类定义为抽象类并定义抽象方法让子类重新定义这些方法当父类是抽象类时父类就是不能实例化所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。

不符合LSP的最常见的情况是父类和子类都是可实例化的非抽象类且父类的方法被子类重新定义这一类的实现继承会造成父类和子类间的强耦合也就是实际上并不相关的属性和方法牵强附会在一起不利于程序扩展和维护。

二、使用里氏替换原则的目的

采用里氏替换原则就是为了减少继承带来的缺点增强程序的健壮性版本升级时也可以保持良好的兼容性。即使增加子类原有的子类也可以继续运行。

三、里氏替换原则与继承多态之间的关系

里氏替换原则和继承多态有关系, 但是他俩并不是一回事. 我们来看看下面的案例

public class Cache {
    public void set(String key, String value) {

    }
}

public class Redis extends Cache {
    @Override
    public void set(String key, String value) {

    }
}


public class Memcache extends Cache {
    @Override
    public void set(String key, String value) {

    }
}

public class CacheTest {
    public static void main(String[] args) {
        // 父类对象都可以接收子类对象
        Cache cache = new Cache();
        cache.set("key123", "key123");

        cache = new Redis();
        cache.set("key123", "key123");

        cache = new Memcache();
        cache.set("key123", "key123");
    }
}

通过上面的例子, 可以看出Cache是父类, Redis 和 Memcache是子类, 他们继承自Cache. 这是继承和多态的思想. 而且这两个子类目前为止也都符合里氏替换原则.可以替换父类出现的任何位置并且原来代码的逻辑行为不变且正确性也没有被破坏。
看最后的CacheTest类, 我们使用父类的cache可以接收任何一种类型的缓存对象, 包括父类和子类.

但如果我们对Redis中的set方法做了长度校验

public class Redis extends Cache{
    @Override
    public void set(String key, String value) {
        if (key == null || key.length() < 10 || key.length() > 100) {
            System.out.println("key的长度不符合要求");
            throw new IllegalArgumentException(key的长度不符合要求);
        }
    }
}

public class CacheTest {
    public static void main(String[] args) {
        // 父类对象都可以接收子类对象
        Cache cache = new Cache();
        cache.set("key123", "key123");

        cache = new Redis();
        cache.set("key123", "key123");
    }
}

如上情况, 如果我们使用父类对象时替换成子类对象, 那么就会抛出异常. 程序的逻辑行为就发生了变化虽然改造之后的代码仍然可以通过子类来替换父类 但是从设计思路上来讲Redis子类的设计是不符合里氏替换原则的。

继承和多态是面向对象语言所提供的一种语法是代码实现的思路而里式替换则是一种思想一种设计原则是用来指导继承关系中子类该如何设计的子类的设计要保证在替换父类的时候不改变原有程序的逻辑以及不破坏原有程序的正确性。

四、里式替换的规则

里氏替换原则的核心就是“约定”父类与子类的约定。里氏替换原则要求子类在进行设计的时候要遵守父类的一些行为约定。这里的行为约定包括:函数所要实现的功能对输入、输出、异常的约定甚至包括注释中一些特殊说明等。

4.1、子类方法不能违背父类方法对输入输出异常的约定

1. 前置条件不能被加强

前置条件即输入参数是不能被加强的就像上面Cache的示例Redis子类对输入参数Key的要求进行了加强此时在调用处替换父类对象为子类对象就可能引发异常。

也就是说子类对输入的数据的校验比父类更加严格那子类的设计就违背了里氏替换原则。

2. 后置条件不能被削弱

后置条件即输出假设我们的父类方法约定输出参数要大于0调用父类方法的程序根据约定对输出参数进行了大于0的验证。而子类在实现的时候却输出了小于等于0的值。此时子类的涉及就违背了里氏替换原则

3. 不能违背对异常的约定

在父类中某个函数约定只会抛出 ArgumentNullException 异常 那子类的设计实现中只允许抛出 ArgumentNullException 异常任何其他异常的抛出都会导致子类违背里氏替换原则。

4.2、子类方法不能违背父类方法定义的功能

public class Product {
    private BigDecimal amount;
    private Calendar createTime;
 
    public BigDecimal getAmount() {
        return amount;
    }
    public void setAmount(BigDecimal amount) {
        this.amount = amount;
    }
 
    public Calendar getCreateTime() {
        return createTime;
    }
    public void setCreateTime(Calendar createTime) {
        this.createTime = createTime;
    }
}
 
public class ProductSort extends Sort<Product> {
 
    public void sortByAmount(List<Product> list) {
        //根据时间进行排序
        list.sort((h1, h2)->h1.getCreateTime().compareTo(h2.getCreateTime()));
    }
}

父类中提供的 sortByAmount() 排序函数是按照金额从小到大来进行排序的而子类重写这个 sortByAmount() 排序函数之后却是是按照创建日期来进行排序的。那子类的设计就违背里氏替换原则。

实际上对于如何验证子类设计是否符合里氏替换原则其实有一个小技巧那就是你可以使用父类的单测来运行子类的代码如果不可以正常运行那么你就要考虑一下自己的设计是否合理了!

4.3、子类必须完全实现父类的抽象方法

如果你设计的子类不能完全实现父类的抽象方法那么你的设计就不满足里氏替换原则。

// 定义抽象类枪
public abstract class AbstractGun{
    // 射击
    public abstract void shoot();
    
    // 杀人
    public abstract void kill();
}

比如我们定义了一个抽象的枪类可以射击和杀人。无论是步枪还是手枪都可以射击和杀人我们可以定义子类来继承父类

// 定义手枪步枪机枪
public class Handgun extends AbstractGun{   
    public void shoot(){  
         // 手枪射击
    }
    
    public void kill(){    
        // 手枪杀人
    }
}
public class Rifle extends AbstractGun{
    public void shoot(){
         // 步枪射击
    }
    
    public void kill(){    
         // 步枪杀人
    }
}

但是如果我们在这个继承体系内加入一个玩具枪就会有问题了因为玩具枪只能射击不能杀人。但是很多人写代码经常会这么写。

public class ToyGun extends AbstractGun{
    public void shoot(){
        // 玩具枪射击
    }
    
    public void kill(){ 
        // 因为玩具枪不能杀人就返回空或者直接throw一个异常出去
        throw new Exception("我是个玩具枪惊不惊喜意不意外刺不刺激?");
    }
}

这时我们如果把使用父类对象的地方替换为子类对象显然是会有问题的(士兵上战场结果发现自己拿的是个玩具)。

而这种情况不仅仅不满足里氏替换原则也不满足接口隔离原则对于这种场景可以通过 ** 接口隔离+委托** 的方式来解决。

五、里氏替换原则的作用

1.里氏替换原则是实现开闭原则的重要方式之一。

2.它克服了继承中重写父类造成的可复用性变差的缺点。

3.它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误降低了代码出错的可能性。

4.加强程序的健壮性同时变更时可以做到非常好的兼容性提高程序的维护性、可扩展性降低需求变更时引入的风险。

尽量不要从可实例化的父类中继承而是要使用基于抽象类和接口的继承。

六、里氏替换原则的实现方法

里氏替换原则通俗来讲就是:子类可以扩展父类的功能但不能改变父类原有的功能。也就是说:子类继承父类时除添加新的方法完成新增功能外尽量不要重写父类的方法。

根据上述理解对里氏替换原则的定义可以总结如下:

1.子类可以实现父类的抽象方法但不能覆盖父类的非抽象方法

2.子类中可以增加自己特有的方法

3.当子类的方法重载父类的方法时方法的前置条件(即方法的输入参数)要比父类的方法更宽松

4.当子类的方法实现父类的方法时(重写/重载或实现抽象方法)方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等

通过重写父类的方法来完成新的功能写起来虽然简单但是整个继承体系的可复用性会比较差特别是运用多态比较频繁时程序运行出错的概率会非常大。

如果程序违背了里氏替换原则则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系重新设计它们之间的关系。

关于里氏替换原则的例子最有名的是“正方形不是长方形”。当然生活中也有很多类似的例子例如企鹅、鸵鸟和几维鸟从生物学的角度来划分它们属于鸟类;但从类的继承关系来看由于它们不能继承“鸟”会飞的功能所以它们不能定义成“鸟”的子类。同样由于“气球鱼”不会游泳所以不能定义成“鱼”的子类;“玩具炮”炸不了敌人所以不能定义成“炮”的子类等。

七、案例分析

7.1、案例一: 两数相减

当使用继承时遵循里氏替换原则。类B继承类A时除添加新的方法完成新增功能P2外尽量不要重写父类A的方法也尽量不要重载父类A的方法。

继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言)实际上是在设定一系列的规范和契约虽然它不强制要求所有的子类必须遵从这些契约但是如果子类对这些非抽象方法任意修改就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

继承作为面向对象三大特性之一在给程序设计带来巨大便利的同时也带来了弊端。比如使用继承会给程序带来侵入性程序的可移植性降低增加了对象间的耦合性如果一个类被其他的类所继承则当这个类需要修改时必须考虑到所有的子类并且父类修改后所有涉及到子类的功能都有可能会产生故障。

class A{
	public int func1(int a, int b){
		return a-b;
	}
}
 
public class Client{
	public static void main(String[] args){
		A a = new A();
		System.out.println("100-50="+a.func1(100, 50));
		System.out.println("100-80="+a.func1(100, 80));
	}
}

运行结果:

100-50=50
100-80=20

后来我们需要增加一个新的功能:完成两数相加然后再与100求和由类B来负责。即类B需要完成两个功能:

  • 两数相减。
  • 两数相加然后再加100。

由于类A已经实现了第一个功能所以类B继承类A后只需要再完成第二个功能就可以了代码如下:

class B extends A{
	public int func1(int a, int b){
		return a+b;
	}
	
	public int func2(int a, int b){
		return func1(a,b)+100;
	}
}
 
public class Client{
	public static void main(String[] args){
		B b = new B();
		System.out.println("100-50="+b.func1(100, 50));
		System.out.println("100-80="+b.func1(100, 80));
		System.out.println("100+20+100="+b.func2(100, 20));
	}
}

类B完成后运行结果:

100-50=150
100-80=180
100+20+100=220

我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法造成所有运行相减功能的代码全部调用了类B重写后的方法造成原本运行正常的功能出现了错误。在本例中引用基类A完成的功能换成子类B之后发生了异常。在实际编程中我们常常会通过重写父类的方法来完成新的功能这样写起来虽然简单但是整个继承体系的可复用性会比较差特别是运用多态比较频繁时程序运行出错的几率非常大。如果非要重写父类的方法比较通用的做法是:原来的父类和子类都继承一个更通俗的基类原有的继承关系去掉采用依赖、聚合组合等关系代替。

7.2、案例二: "几维鸟不是鸟"

需求分析: 鸟通常都是会飞的, 比如燕子每小时120千米, 但是新西兰的几维鸟由于翅膀退化不会飞. 假如要设计一个实例计算这两种鸟飞行 300 千米要花费的时间。显然拿燕子来测试这段代码结果正确能计算出所需要的时间;但拿几维鸟来测试结果会发生“除零异常”或是“无穷大”明显不符合预期其类图如图 1 所示。

源码如下:

/**
 * 鸟
 */
public class Bird {
    // 飞行的速度
    private double flySpeed;

    public void setFlySpeed(double flySpeed) {
        this.flySpeed = flySpeed;
    }

    public double getFlyTime(double distance) {
        return distance/flySpeed;
    }
}

/**
 * 燕子
 */
public class Swallow extends Bird{
}

/**
 * 几维鸟
 */
public class Kiwi extends Bird {
    @Override
    public void setFlySpeed(double flySpeed) {
        flySpeed = 0;
    }
}

/**
  * 测试飞行耗费时间
  */
public class BirdTest {
    public static void main(String[] args) {
        Bird bird1 = new Swallow();
        Bird bird2 = new Kiwi();
        bird1.setFlySpeed(120);
        bird2.setFlySpeed(120);
        System.out.println("如果飞行300公里:");
        try {
            System.out.println("燕子花费" + bird1.getFlyTime(300) + "小时.");
            System.out.println("几维花费" + bird2.getFlyTime(300) + "小时。");
        } catch (Exception err) {
            System.out.println("发生错误了!");
        }
    }
}

运行结果:

如果飞行300公里:
燕子花费2.5小时.
几维花费Infinity小时。

程序运行错误的原因是:几维鸟类重写了鸟类的 setSpeed(double speed) 方法这违背了里氏替换原则。正确的做法是:取消几维鸟原来的继承关系定义鸟和几维鸟的更一般的父类如动物类它们都有奔跑的能力。几维鸟的飞行速度虽然为 0但奔跑速度不为 0可以计算出其奔跑 300 千米所要花费的时间。其类图如图 2 所示。

源代码实现如下

/**
 * 动物
 */
public class Animal {
    private double runSpeed;

    public double getRunTime(double distance) {
        return distance/runSpeed;
    }

    public void setRunSpeed(double runSpeed) {
        this.runSpeed = runSpeed;
    }
}


/**
 * 鸟
 */
public class Bird {
    // 飞行的速度
    private double flySpeed;

    public void setFlySpeed(double flySpeed) {
        this.flySpeed = flySpeed;
    }

    public double getFlyTime(double distance) {
        return distance/flySpeed;
    }
}

/**
 * 燕子
 */
public class Swallow extends Bird {
}

/**
 * 几维鸟
 */
public class Kiwi extends Animal {
    @Override
    public void setRunSpeed(double runSpeed) {
        super.setRunSpeed(runSpeed);
    }
}

/**
  * 测试飞行耗费时间
  */
public class BirdTest {
    public static void main(String[] args) {
        Bird bird1 = new Swallow();
        Animal bird2 = new Kiwi();
        bird1.setFlySpeed(120);
        bird2.setRunSpeed(110);
        System.out.println("如果飞行300公里:");
        try {
            System.out.println("燕子花费" + bird1.getFlyTime(300) + "小时.");
            System.out.println("几维鸟花费" + bird2.getRunTime(300) + "小时。");
        } catch (Exception err) {
            System.out.println("发生错误了!");
        }
    }
}

运行结果

如果飞行300公里:
燕子花费2.5小时.
几维鸟花费2.727272727272727小时。

八、总结

面向对象的编程思想中提供了继承和多态是我们可以很好的实现代码的复用性和可扩展性但继承并非没有缺点因为继承的本身就是具有侵入性的如果使用不当就会大大增加代码的耦合性而降低代码的灵活性增加我们的维护成本然而在实际使用过程中却往往会出现滥用继承的现象而里氏替换原则可以很好的帮助我们在继承关系中进行父子类的设计。


相关文章

猜您喜欢

网友评论

Copyright 2020 www.Musicdownload3mp.com 【飞音下载站】 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 点此查看联系方式