设计模式中的里氏替换原则(LSP)

我们在做系统设计时,经常会定义一个接口或抽象类,然后编码实现,调用类则直接传入接口或抽象类,其实这已经使用了里氏替换原则。

里氏替换

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

  • 简单理解
    任何基类可以出现的地方,子类一定可以出现

  • 详细描述:
    在代码中将一个基类对象替换成它的子类对象,程序不会产生任何错误和异常,反过来则不成立,如果一个类实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。

    例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。

  • 场景分析:

    例如有两个类,一个类为BaseClass,另一个是SubClass类,并且SubClass类是BaseClass类的子类,那么一个方法如果可以接受一个BaseClass类型的基类对象base的话,如:method1(base),那么它必然可以接受一个BaseClass类型的子类对象sub,method1(sub)能够正常运行。反过来的代换不成立,如一个方法method2接受BaseClass类型的子类对象sub为参数:method2(sub),那么一般而言不可以有method2(base)

    class BaseClass {
        ...
    }
    class SubClass extends BaseClass{
       ...
    }
    class OtherClass {
        //此处的BaseClass能够替换成子类的SubClass类型对象
        public void method1(BaseClass base){
            
        }
        //反过来,此处的SubClass对象不能够替换为BaseClass对象
        public void method2(SubClass sub){
            
        }
    }
    

    我们在做系统设计时,经常会定义一个接口或抽象类,然后编码实现,调用类则直接传入接口或抽象类,其实这已经使用了里氏替换原则。

    我们举一个拿手机打电话的例子,结构组成如下
    设计模式中的里氏替换原则(LSP)_第1张图片

    abstract class AbstractPhone {
    	//打电话方法
    	public abstract void call();
    }
    
    class FixedPhone extends AbstractPhone {
    	@Override
    	public void call() {
    		System.out.println("用固定电话打电话");
    	}
    }
    
    class MobilePhone extends AbstractPhone {
    	@Override
    	public void call() {
    		System.out.println("用移动电话打电话");
    	}
    }
    
    //用户打电话
    class Person {
    	private AbstractPhone phone;
    
    	public void setPhone(AbstractPhone phone) {
    		this.phone = phone;
    	}
    	
    	public void startCall() {
    		System.out.println("用户开始打电话");
    		this.phone.call();
    	}
    }
    
    public class Client {
    	public static void main(String[] args) {
    		Person person = new Person();
    		
    		//设置用户拿固定电话
    		person.setPhone(new FixedPhone());
    		person.startCall();
    		
    		//设置用户拿移动电话
    		person.setPhone(new MobilePhone());
    		person.startCall();
    	}
    }
    

    里氏替换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象

    里氏替换原则的另一层含义是子类可以扩展父类的功能,但不能改变父类原有的功能,主要作用就是规范继承时子类的一些书写规则,接下来我们看看继承规范时的书写规则有哪些。

  • 里氏替换原则继承规范

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

    • 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法(视情况而定)

      以下例举出Java小白开发者的编码,看看覆盖了父类的非抽象方法后会带来什么后果

      //类A完成,两个数相加的功能
      class A {
          public int fun1(int a,int b){
              return a + b;
          }
      }
      
      //Demo执行
      public class Demo3 {
          public static void main(String[] args){
              A a = new A();
              System.out.println("5+2的结果为:" + a.fun1(5,2));
          }
      }
      
      //程序输出:5+2的结果为:7
      

      随着业务的发展,我们需要新增一个功能,完成2个数相减,并对其结果再减去10,考虑到扩展性,我们不应该改动原来的代码,所以我们重新定义一个类B完成,如下。

      //类B,在类A的基础上扩展
      class B extends A {
      	public int fun1(int a,int b){
              return a - b;
          }
      	
      	public int fun2(int a,int b){
      		return fun1(a , b) - 10;
      	}
      }
      
      //Demo执行
      public class Demo3 {
          public static void main(String[] args){
              B b = new B();
              System.out.println("25+2的结果为:" + b.fun1(25,2));
              System.out.println("18+2的结果为:" + b.fun1(18,2));
              System.out.println("18-2-10的结果为:" + b.fun2(18,2));
          }
      }
      //程序输出:25+2的结果为:23
      //程序输出:18+2的结果为:16
      //程序输出:18-2-10的结果为:6
      

      此时你会发现,我们原本以为在类A的基础上去扩展的类B会正确输出,但实际类B已经覆盖了类A中已实现的方法,所以出现了错误的结果。虽然这段代码我们能够看出来有问题,但在实际开发中,在不知情的情况下去覆盖了父类的非抽象方法,这会带来意想不到的错误。

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

      这个规则我们通过继承就能够知道,子类可以扩展自己的行为和属性,那为什么要在里氏替换中提出呢?主要是因为里氏替换原则是说子类可以胜任父类的任何工作,但父类不一定能够替换子类,所以提出该规则

      我们在上述打电话的案例中新增一个例子,比如移动电话还有其子类产品,比如OPPO、HuaWei、Mi
      设计模式中的里氏替换原则(LSP)_第2张图片

      //移动电话的子类,华为手机
      class HuaWei extends MobilePhone {
      	public void unlock() {
      		System.out.println("打电话之前解锁");
      	}
      	
      	public void call() {
      		System.out.println("华为手机打电话");
      	}
      }
      
      //动作处理,由于扩展了属性,所以不再适合在之前的Person类中更改,新建一个User类直接处理
      class User{
      	public void startCall(HuaWei huaWei) {
      		huaWei.unlock();
      		huaWei.call();
      	}
      }
      
      //客户端调用类
      public class Client {
      	public static void main(String[] args) {
              //子类可以直接调用
      		User user = new User();
      		user.startCall(new HuaWei());
              
              //如果现在指定了子类,你要传入父类,会出现编译错误,即使向下强转也会出运行异常
              User user = new User();
      		user.startCall((HuaWei)(new MobilePhone()));//出现异常
      	}
      }
      
    • 当子类覆盖或实现父类已实现的方法时,子类方法的输入参数(形参)要比父类方法的输入参数更宽松,不能相同。(其实是要求子类不允许覆盖父类已实现的方法)

      有如下代码,运行结果为:父类被执行

      //父类
      class Father {
      	public void doSomething(HashMap<String, String> map) {
      		System.out.println("父类被执行");
      	}
      }
      
      //子类
      class Son extends Father {
      	public void doSomething(Map<String, String> map) {
      		System.out.println("子类被执行");
      	}
      }
      
      class Client{
      	public static void main(String[] args) {
      		Father father = new Father();
      		HashMap<String, String> map = new HashMap<String, String>();
      		father.doSomething(map);
      	}
      }
      

      根据里氏替换原则,父类出现的地方,子类也是可以出现的。我们把Client代码修改如下:

      class Client{
      	public static void main(String[] args) {
      		// 父类存在的地方,子类应该可以存在,而且结果应与父类相同
      		//Father father = new Father();
      		Son father = new Son();
      		HashMap<String, String> map = new HashMap<String, String>();
      		father.doSomething(map);
      	}
      }
      

      运行结果依然是:父类被执行

      结果一样,父类的方法的输入参数是HashMap类型,子类的方法输入参数是Map类型,也就是说子类的输入参数类型范围扩大了,子类代替父类,子类的方法不被执行,这是正确的,如果你想让子类的方法运行,就必须覆写父类的方法。

      如果,我们反过来,把父类的输入参数类型放大,子类的输入参数类型缩小,让子类的输入参数类型小于父类的输入参数类型,看看会出现什么情况?

      //错误实例
      //父类
      class C {
          public void fun(Map<String,String> map){
              System.out.println("父类被执行...");
          }
      }
      //子类
      class D extends C{
          public void fun(HashMap<String,String> map){
              System.out.println("子类被执行...");
          }
      }
      
      public class Client {
      	public static void main(String[] args) {
              System.out.print("父类的运行结果:");
              C c = new C();
              HashMap<String,String> map=new HashMap<String,String>();
              c.fun(map);
              
              //父类存在的地方,都可以用子类替代
              //子类B替代父类A
              System.out.print("子类替代父类后的运行结果:");
              D d=new D();
              d.fun(map);
      	}
      }
      
      //输出结果:
      父类的运行结果:父类被执行...
      子类替代父类后的运行结果:子类被执行...
      

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

    • 当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格(范围更小)。

      abstract class E {
          public abstract Map<String,String> fun();
      }
       
      class F extends E{
      	
          @Override
          public HashMap<String,String> fun(){
              HashMap<String,String> f = new HashMap<String,String>();
              f.put("f","子类被执行...");
              return f;
          }
      }
      
      public class Client {
      	public static void main(String[] args) {
      		E f = new F();
              System.out.println(f.fun());
      	}
      }
      

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

  • 以上就是里氏替换原则知识点,有一定的参考作用,但无需严格遵守,在实际开发中应根据实际情况来遵守

你可能感兴趣的:(#,设计模式精讲,设计模式,里氏替换原则,java)