美文网首页开发入门者码农的世界程序员
六大设计原则之里氏替换原则(LSP)

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

作者: 爱一直在线 | 来源:发表于2018-03-06 22:13 被阅读88次

    在学习java类的继承时,我们知道继承有一些优点

    1. 子类拥有父类的所有方法和属性,从而可以减少创建类的工作量。
    2. 提高了代码的重用性。
    3. 提高了代码的扩展性,子类不但拥有了父类的所有功能,还可以添加自己的功能。

    但又有点也同样存在缺点

    1. 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
    2. 降低了代码的灵活性。因为继承时,父类会对子类有一种约束。
    3. 增强了耦合性。当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。有时修改了一点点代码都有可能需要对打断程序进行重构。

    如何扬长避短呢?方法是引入里氏替换原则

    定义

    • 第一种定义,也是最正宗的定义:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
      如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。

    • 第二种定义:Functions that use pointers or references to base classes must be able to useobjects of derived classes without knowing it.
      所有引用基类的地方必须能透明地使用其子类的对象。

    第二种定义比较通俗,容易理解:只要有父类出现的地方,都可以用子类来替代,而且不会出现任何错误和异常。但是反过来则不行,有子类出现的地方,不能用其父类替代。

    四层含义

    里氏替换原则对继承进行了规则上的约束,这种约束主要体现在四个方面:

    1. 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
    2. 子类中可以增加自己特有的方法。
    3. 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
    4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

    下面对以上四个含义进行详细的解释:

    子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法

    在我们做系统设计时,经常会设计接口或抽象类,然后由子类来实现抽象方法,这里使用的其实就是里氏替换原则。若子类不完全对父类的方法进行实例化,那么子类就不能被实例化,那么这个接口或抽象类就毫无存在的意义了。

    里氏替换原则规定,子类不能覆写父类已实现的方法。父类中已实现的方法其实是一种已定好的规范和契约,如果我们随意的修改了它,那么可能会带来意想不到的错误。

    下面举例说明一下子类覆写了父类方法带来的后果。

    public class A {
        public void fun(int a,int b){
            System.out.println(a+"+"+b+"="+(a+b));
        }
    }
    
    public class B extends A{
        @Override
        public void fun(int a,int b){
            System.out.println(a+"-"+b+"="+(a-b));
        }
    }
    
    public class demo {
        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);
        }
    }
    

    运行结果:
    父类的运行结果
    1+2=3
    子类替代父类后的运行结果
    1-2=-1

    我们想要的结果是“1+2=3”。可以看到,方法重写后结果就不是了我们想要的结果了,也就是这个程序中子类B不能替代父类A。这违反了里氏替换原则原则,从而给程序造成了错误。

    有时候父类有多个子类,但在这些子类中有一个特例。要想满足里氏替换原则,又想满足这个子类的功能时,有的伙伴可能会修改父类的方法。但是,修改了父类的方法又会对其他的子类造成影响,产生更多的错误。这是怎么办呢?我们可以为这个特例创建一个新的父类,这个新的父类拥有原父类的部分功能,又有不同的功能。这样既满足了里氏替换原则,又满足了这个特例的需求。

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

    这个很容易理解,子类继承了父类,拥有了父类和方法,同时还可以定义自己有,而父类没有的方法。这是在继承父类方法的基础上进行功能的扩展,符合里氏替换原则。

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

    运行结果:
    父类的运行结果:1+2=3
    子类替代父类后的运行结果:1+2=3
    这是子类的新方法...

    当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松

    先看一段代码:

    import java.util.HashMap;
    public class A {
        public void fun(HashMap map){
            System.out.println("父类被执行...");
        }
    }
    
    import java.util.Map;
    public class B extends A{
        public void fun(Map map){
            System.out.println("子类被执行...");
        }
    }
    
    import java.util.HashMap;
    public class demo {
        public static void main(String[] args){
            System.out.print("父类的运行结果:");
            A a=new A();
            HashMap map=new HashMap();
            a.fun(map);
            //父类存在的地方,可以用子类替代
            //子类B替代父类A
            System.out.print("子类替代父类后的运行结果:");
            B b=new B();
            b.fun(map);
        }
    }
    

    运行结果:
    父类的运行结果:父类被执行...
    子类替代父类后的运行结果:父类被执行...

    我们应当主意,子类并非重写了父类的方法,而是重载了父类的方法。因为子类和父类的方法的输入参数是不同的。子类方法的参数Map比父类方法的参数HashMap的范围要大,所以当参数输入为HashMap类型时,只会执行父类的方法,不会执行子类的重载方法。这符合里氏替换原则。

    但如果我将子类方法的参数范围缩小会怎样?看代码:

    import java.util.Map;
    public class A {
        public void fun(Map map){
            System.out.println("父类被执行...");
        }
    }
    
    import java.util.HashMap;
    public class B extends A{
        public void fun(HashMap map){
            System.out.println("子类被执行...");
        }
    }
    
    import java.util.HashMap;
    
    public class demo {
        public static void main(String[] args){
            System.out.print("父类的运行结果:");
            A a=new A();
            HashMap map=new HashMap();
            a.fun(map);
            //父类存在的地方,可以用子类替代
            //子类B替代父类A
            System.out.print("子类替代父类后的运行结果3");
            B b=new B();
            b.fun(map);
        }
    }
    

    运行结果:
    父类的运行结果:父类被执行...
    子类替代父类后的运行结果:子类被执行...

    呵呵!在父类方法没有被重写的情况下,子方法被执行了,这样就引起了程序逻辑的混乱。所以子类中方法的前置条件必须与父类中被覆写的方法的前置条件相同或者更宽松。

    当子类的方法实现父类的(抽象)方法时,方法的后置条件(即方法的返回值)要比父类更严格

    示例代码:

    import java.util.Map;
    public abstract class A {
        public abstract Map fun();
    }
    
    import java.util.HashMap;
    public class B extends A{
        @Override
        public HashMap fun(){
            HashMap b=new HashMap();
            b.put("b","子类被执行...");
            return b;
        }
    }
    
    import java.util.HashMap;
    public class demo {
        public static void main(String[] args){
            A a=new B();
            System.out.println(a.fun());
        }
    }
    

    运行结果:
    {b=子类被执行...}

    若在继承时,子类的方法返回值类型范围比父类的方法返回值类型范围大,在子类重写该方法时编译器会报错。

    总结

    java采用的单继承相较于c++的多继承,总体上来看是“利”多于“弊”的。采用里氏替换原则可以让“利”的因素发挥最大的作用,并减少“弊”带来的诸多麻烦。

    相关文章

      网友评论

        本文标题:六大设计原则之里氏替换原则(LSP)

        本文链接:https://www.haomeiwen.com/subject/atfbuxtx.html