六大原则(2)——里氏替换原则

里氏替换原则

概述

定义

严格的定义:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。

通俗的定义:所有引用基类的地方必须能透明地使用其子类的对象。

更通俗的定义:子类可以扩展父类的功能,但不能改变父类原有的功能(一方面指不改变父类原功能的逻辑,另外一方面也指不改变该方法的含义)。

对于基类中定义的所有子程序,用在它的任何一个派生类中时的含义都应该是相同的。这样继承才不会增加复杂度,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。如果我们必须要不断地思考不同派生类的实现在语义上的差异,继承就只会增加复杂度了。

问题由来

有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

解决方案

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

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

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

四层含义

子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。

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

举个栗子

public class TestFirst {
    public static void main(String args[]){
        Operator operator = new Operator();
        System.out.println("1 + 2 = " + operator.add(1 , 2));
        operator = new ChildOperator();
        System.out.println("1 + 2 = " + operator.add(1 , 2));
    }

    static class Operator{
        public int add(int a, int b){
            return a + b;
        }
    }

    static class ChildOperator extends Operator{
        public int add(int a, int b){
            return a - b;
        }
    }

}

输出

1 + 2 = 3
1 + 2 = -1

上面的例子有两个问题:
- 问题1:改变了父类原有功能的逻辑

子类改变了父类的逻辑,导致输出的结果不对,假设operator是一个全局对象,在很多出地方使用,一直都是采用了1 + 2 = 3这种逻辑,后来产品需求改变了,需要一种新的模式是1 + 2 = -1这种,但是在很多外面地方需要采取以前1+2=3的逻辑的地方却产生了1+2=-1,这就出现了问题。

  • 问题2:改变了父类原有功能的含义

原有父类的add方法的含义是两个数字相加,但是子类的逻辑是两个数字相减,导致函数的含义发生改变,然而外界对这个函数的理解往往以为是前者,导致代码功能出现问题。

子类中可以增加自己特有的方法(拥有个性)

在继承父类属性和方法的同时,每个子类也都可以有自己的个性,在父类的基础上扩展自己的功能。前面其实已经提到,当功能扩展时,子类尽量不要重写父类的方法,而是另写一个方法,所以对上面的代码加以更改,使其符合里氏替换原则,代码如下

static class ChildOperator2 extends Operator{
        public int subtract(int a, int b){
            return a - b;
        }
    }

如果继承为了不是拓展,那将毫无意义,上面例子中子类拓展了减法功能

覆盖或实现父类的方法时输入参数范围应该大于等于父类

个人的理解是,父亲能做的,子类应该也能完成,即向上兼容,所以子类的参数范围应该不小于父类,所以重写的时候相同方法应该参数范围大于父类,当然就算是小于父类,代码上依然能正常执行,但是对于继承就感觉不太好了,子类变得越来越弱了。

    static class Food{
        public String getFood(){
            return "unkown food";
        }
    }

    static class MeatFood extends Food{
        @Override
        public String getFood() {
            return "meat";
        }
    }

    static class VegetablesFood extends Food{
        @Override
        public String getFood() {
            return "vegetables";
        }
    }

    static class Dog{
        public void eat(MeatFood food) {
            System.out.println("dog eat " + food.getFood());
        }
    }

    static class Hashiqi extends Dog{
        public void eat(Food food) {//放大范围
            System.out.println("hashiqi eat " + food.getFood());
        }
    }

    public static void main(String args[]){

        Hashiqi animal = new Hashiqi();
        animal.eat(new MeatFood());
        animal.eat(new VegetablesFood());
    }

输出

dog eat meat//调用的是父类方法
hashiqi eat vegetables//调用的是子类方法

当然如果改成下面:

static class Hashiqi extends Dog{
        public void eat(FishFood food) {//缩小范围
            System.out.println("hashiqi eat " + food.getFood());
        }
    }

    public static void main(String args[]){

        Hashiqi animal = new Hashiqi();
        animal.eat(new MeatFood());
        animal.eat(new FishFood());

    }

输出

dog eat meat
hashiqi eat fish

虽然也是正常执行,但是同样的eat方法,子类的方法能吃的确更少了,这就是典型的“挑食”,感觉不好

4.覆写或实现父类的方法时输出结果范围小于等于父类返回结果

举例如下:

    interface Tool{
        String getTool();
    }

    static class Leg implements Tool{

        @Override
        public String getTool() {
            return "legs";
        }

        @Override
        public String toString() {
            return getTool();
        }
    }

    static class FourLeg extends Leg{
        @Override
        public String getTool() {
            return "four legs";
        }
    }

    static class Wing implements Tool{

        @Override
        public String getTool() {
            return "wings";
        }

        @Override
        public String toString() {
            return getTool();
        }
    }


    static class Animal{
        public Leg walk(){
            return new Leg();
        }
    }

    static class Dog extends Animal{

        @Override
        public FourLeg walk(){
            return new FourLeg();
        }

        public Tool walk(int i){
            return new Wing();
        }
    }

    public static void main(String args[]){
        Dog dog = new Dog();
        System.out.println("dog move with " + dog.walk());
        System.out.println("dog move with " + dog.walk(0));
    }

输出

dog move with four legs
dog move with wings//很奇怪啊,狗有翅膀

上面例子中,重写父类方法,如Dog.walk()方法,返回值是可以比父类小(当然,如果返回值比父类大,编译器一般直接会报错了),当然重载的时候不会报错,如Dog.walk(0),但是返回值范围变大了,当用户调用这个方法的时候居然发现狗有翅膀,就显得很不合逻辑了。比如某个全局变量,一直调用A方法,返回一种类型B,后来A方法被重载了,新方法返回了C类型,但客户端(未必知道)去调用的时候,本来想着还是返回B,却发现返回了C,就很尴尬了。

你可能感兴趣的:(6大原则,设计原则,里氏替换原则,六大原则)