设计模式-里氏替换原则

一家店一开始只卖酒类,只让成年人进入消费。后续增加了奶制品,宣称可以让未成年人进店消费。我们都知道显然是无法营业的,因为这家店扩展业务之后包含了未成年不允许涉及的东西,甚至会导致原先的业务(酒类)无法开展。这就是一个未遵循里氏替换原则的反例。

1. 概述

里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。理解里氏替换原则对程序的健壮性、扩展性、兼容性至关重要。

1.1. 依赖倒置原则的作用

  • 里氏替换原则是实现开闭原则的重要方式之一。
  • 它克服了继承中重写父类造成的可复用性变差的缺点。
  • 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
  • 加强程序健壮性,同时变更时可以做到非常好的兼容性,提高程序维护性、可扩展性,降低需求变更时引入的风险。

1.2. 程序举例

几维鸟不是鸟


package com.skystep.design.unlsp;

public class Bird {

    double flySpeed;

    public void setSpeed(double speed) {
        flySpeed = speed;
    }

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




package com.skystep.design.unlsp;

class Swallow extends Bird {
}

package com.skystep.design.unlsp;

class BrownKiwi extends Bird {
    public void setSpeed(double speed) {
        flySpeed = 0;
    }
}


package com.skystep.design.unlsp;

public class Client {

    public static void main(String[] args) {
        Bird swallow1 = new Swallow();
        Bird brownKiwi1 = new BrownKiwi();
        swallow1.setSpeed(120);
        brownKiwi1.setSpeed(120);
        System.out.println("如果飞行300公里:");
        try {
            System.out.println("燕子将飞行" + swallow1.getFlyTime(300) + "小时.");
            System.out.println("几维鸟将飞行" + brownKiwi1.getFlyTime(300) + "小时。");
        } catch (Exception err) {
            System.out.println("发生错误了!");
        }
    }
}


程序运行错误的原因是:几维鸟类重写了鸟类的 setSpeed(double speed) 方法,这违背了里氏替换原则。子类继承了父类的方法,但不得重写(覆盖)父类的非抽象(已实现)方法。否则大概率会影响原先的业务,且原先调用者无法知道该方法被重写。因为一般情况下,我们认为父类可以使用的地方,替换成子类业务应答不受影响。

1.3. 总结

继承提高代码的重用性,子类拥有父类的方法和属性;提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性;这是继承带给我们的好处,但是如果在编写程序中,对继承不做任何限制,任意继承,子类随意重写父类的方法,很可能会造成非常糟糕的结果,要重构大量的代码。所以我们提倡编写继承类时需要遵循:任何基类可以出现的地方,子类一定可以出现

2. 实现方法

2.1. 遵循规则

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
  • 子类中可以增加自己特有的方法
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
  • 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等

2.2. 具体实现

2.2.1 子类实现父类的抽象方法但不能覆盖父类的具体方法

package com.skystep.design.unlsp;

public class A {
    public void fun(int a, int b) {
        System.out.println(a + "+" + b + "=" + (a + b));
    }
}


 
package com.skystep.design.unlsp;

public class B extends A {
    @Override
    public void fun(int a, int b) {
        System.out.println(a + "-" + b + "=" + (a - b));
    }
}



package com.skystep.design.unlsp;

public class Demo1 {
    public static void main(String[] args) {
        System.out.println("父类的运行结果");
        A a = new A();
        a.fun(1, 2);
        //父类存在的地方,可以用子类替代
        //子类B替代父类A
        System.out.println("子类替代父类后的运行结果");
        B b = new B();
        b.fun(1, 2);
    }
}

显然如果覆盖了父类的具体方法,那么在使用父类的地方换成其子类时便会影响原有的功能特性。

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

package com.skystep.design.unlsp.demo2;

public class A {
    public void fun(int a, int b) {
        System.out.println(a + "+" + b + "=" + (a + b));
    }
}



package com.skystep.design.unlsp.demo2;

public class B extends A {
    public void newfun() {
        System.out.println("这是子类的新方法...");
    }
}



package com.skystep.design.unlsp.demo2;

public class Demo2 {
    public static void main(String[] args) {
        System.out.print("父类的运行结果:");
        A a = new A();
        a.fun(4, 5);
        //父类存在的地方,可以用子类替代
        //子类B替代父类A
        System.out.print("子类替代父类后的运行结果:");
        B b = new B();
        b.fun(4, 5);
        //子类B的新方法
        b.newfun();
    }
}

如果在原先的功能P基础上新增功能P1,可以在子类上新增特性,在基类使用的地方替换成新的子类,并且调用新的功能特性以满足新功能要求。当然前提是每个功能符合单一原则。

2.2.3. 方法的输入参数要比父类的方法更宽松
2.2.4. 方法的返回值要比父类的方法更严格或相等
posted @ 2021-10-09 12:19  yaomianwei  阅读(35)  评论(0编辑  收藏  举报