基于LSP原则对方法的一点相关

方法

  方法,或者叫类的行为,定义了类能够实现的功能。在任何程序设计语言中,方法一个重要的特性就是名字的运用。我们创建一个对象时,会分配到一个保存区域的名字。方法名代表的是一种具体的行动。通过名字描述自己的系统,可使自己的程序更易人们理解和修改。它就像写散文——目的是与读者沟通。

构造方法

  构造方法是与一种与类同名且无返回值类型的方法。构造方法在实例化一个对象的同时进行编译,它分为无参构造和含参构造。无参构造顾名思义就是不含参数类型和参数的方法。

Static关键字

方法的实现

  通常在开发中,我们会定义一些接口或者抽象类,然后编码实现。在实现这些接口或者抽象类时,会进行方法的重载(overload)和覆写(override)。而重载和覆写区别在就在于重载是方法名相同,参数类型和参数个数不同;覆写是方法名和参数类型以及个数与父类方法完全相同。

子类必须完全实现父类的方法

  根据LSP原则,在此定义一个汽车类图

  汽车能够启动,而在车主类中定义一个方法racing,进行飙车,具体用什么车来飙车,调用车型的时候才知道,AbstractCar的代码如下:

1 public abstract class AbstratCar {
2 
3     //汽车能够启动
4     public abstract void drive();
5     
6 }

  跑车的实现类

1 public class SportsCar extends AbstratCar{
2     //跑车速度快
3     @Override
4     public void drive() {
5         // TODO Auto-generated method stub
6         System.out.println("跑车启动");
7     }
8 
9 }

  拖拉机的实现类

1 public class Tractor extends AbstratCar{
2     //拖拉机跑起来冒烟
3     @Override
4     public void drive() {
5         // TODO Auto-generated method stub
6         System.out.println("拖拉机启动");
7     }
8 
9 }

  卡车的实现类

1 public class Truck extends AbstratCar{
2     //卡车能拉货
3     @Override
4     public void drive() {
5         // TODO Auto-generated method stub
6         System.out.println("卡车启动");
7     }
8 
9 }

  司机的实现类

public class Owner {
    //定义司机的汽车
    private AbstracCar car;
    //给司机一张车
    public void setCar(AbstracCar _car){
        this.car = _car;
    }
    
    public void racing(){
        car.drive();
        System.out.println("老司机正在飙车");
    }
}

 

  汽车能够启动,但汽车是抽象的,具体是什么车启动需要在开车的时候(也就是场景中)才能通过drive方法确定,场景类Client实现入下:

 1 public class Client {
 2 
 3     public static void main(String[] args) {
 4         //产生一个老司机
 5         Owner oldDriver = new Owner();
 6         //给老司机一张车
 7         oldDriver.setCar(new SportsCar());
 8         oldDriver.racing();
 9         
10     }
11 
12 }

  运行结果如下:

跑车启动
老司机正在飙车

  在这个程序中,我们给了老司机一张跑车,然后老司机就开始飙车了。如果老司机要开拖拉机,直接修改oldDriver.setCar(new SportsCar())为oldDriver.setCar(new (Tractor))即可,在编写程序时车主类根本不用知道是哪一个型号的车(子类)被传入。

  在类中调用其它类是务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背LSP原则。

  那再来看一看当出现一张玩具车时的定义。修改后的类图如下:

  在这里有个问题,玩具车是不能用来飙车的,所以不能写在drive方法中。

  玩具车实现

1 public class ToyCar extends AbstratCar{
2     // 玩具车不能启动,但编译器要求实现这个方法,只能虚构一个
3     @Override
4     public void drive() {
5         // 玩具车不能启动,故此方法不实现
6         
7     }
8 
9 }

  由于引入了新的子类,场景类中也使用了该类,Client稍作修改,如下:

 1 public class Client {
 2 
 3     public static void main(String[] args) {
 4         //产生一个老司机
 5         Owner oldDriver = new Owner();
 6         //给老司机一张车
 7         oldDriver.setCar(new ToyCar());
 8         oldDriver.racing();
 9         
10     }
11 
12 }

  运行结果如下

老司机正在飙车

  然而玩具车无法启动,因此不能够飙车,此时正常的业务逻辑无法继续运行,在此有两种解决办法:

    1.在Owner类中增加判断,如果是玩具车,就不能飙车。这可以解决问题,但是在程序中没增加一个类,所有与这个父类有关系的类就必须修改,因此这个方法行不通。

    2.ToyCar脱离继承,建立一个独立的父类,可以与AbastrctCar建立关联委托关系,如下图:

  在AbstractToy中奖部分数据的处理委托给AbstractCar中处理,然后两个基类下的子类自由延展,互不影响。

  按照Java三大特征,继承就是让子类拥有父类的方法和属性,然后可以重写父类的方法。按照继承原则,上面的玩具车继承AbstractCar是完全没有问题的,但是在具体的应用场景中需要考虑下面这问题:子类是否能够完整的实现父类的业务,否则就会想上面一样出现业务逻辑混乱的问题。

  如果子类不能完整的实现父类方法,或者父类的某些方法在子类中已经发生畸变,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。

子类可以有自己的个性

  子类能够有自己的方法和属性,在LSP原则下,子类出现的地方,父类未必能胜任。依然以上文的汽车为例,将法拉第和奔驰引入跑车的子类。

  奔驰继承SportsCar类,霸道总裁(Ceo)直接用奔驰进行飙车。

  奔驰车类实现

public class Benz extends SportsCar{
    //豪车上一般会带个妹子
    public void girl(){
        System.out.println("香车美人");
    }
    
    public void drive(){
        System.out.println("奔驰启动");
    }
}

  霸道总裁类实现

 1 public class Ceo {
 2     private Benz benz;
 3     
 4     public void setCar(Benz benz){
 5         this.benz = benz;
 6     }
 7     
 8     public void racing(){
 9         //上车前先带个妹子
10         benz.girl();
11         //开车开车
12         benz.drive();
13     }
14 }

 

  业务场景类实现

 1 public class Client {
 2 
 3     public static void main(String[] args) {
 4         //产生一个老司机
 5         Ceo oldDriver = new Ceo();
 6         //给老司机一张车
 7         oldDriver.setCar(new Benz());
 8         oldDriver.racing();
 9         
10     }
11 
12 }

  运行结果如下

香车美人
奔驰启动

  在这里,系统直接调用了子类,也就是直接将子类传递进来,那么在这个时候能不能直接使用父类传递进来呢?修改一下代码如下:

 1 public class Client {
 2 
 3     public static void main(String[] args) {
 4         //产生一个老司机
 5         Ceo oldDriver = new Ceo();
 6         //给老司机一张车
 7         oldDriver.setCar((Benz)new SportsCar());
 8         oldDriver.racing();
 9         
10     }
11 
12 }

  运行结果如下:

Exception in thread "main" java.lang.ClassCastException: demo7.SportsCar cannot be cast to demo7.Benz
    at demo7.Client.main(Client.java:11)

  这个时候会抛出java.lang.ClassCastException异常,这也就是常说的,向下转型(downcast)是不安全的,也就是说,子类出现的地方父类未必可以出现。

覆盖或实现父类的方法时输入的参数可以被放大

  方法中的输入参数称为前置条件,在继承关系中前置条件范围的不同会造成什么关系呢,让我们测试一下。

1 public class Father {
2     public Collection doSomething(HashMap map){
3         System.out.println("父类被执行");
4         
5         return map.values();
6         
7     }
8 }

  定义父类代码

public class Son extends Father{
    public Collection doSomething(Map map){
        System.out.println("子类被执行");
        return map.values();
    }
}

  定义子类

 1 public class Client {
 2 
 3     public static void invoker(){
 4         //父类存在的地方,子类就应该能够存在
 5         Father f = new Father();
 6         HashMap map = new HashMap();
 7         f.doSomething(map);
 8     }
 9     
10     public static void main(String[] args) {
11         invoker();
12     }
13 
14 }

  定义场景类

父类被执行

  运行结果如上。根据LSP原则,父类存在的地方子类就应该能存在,如果将父类替换替换一下,会有什么结果呢。

 1 public class Client {
 2 
 3     public static void invoker(){
 4         //父类存在的地方,子类就应该能够存在
 5         Son f = new Son();
 6         HashMap map = new HashMap();
 7         f.doSomething(map);
 8     }
 9     
10     public static void main(String[] args) {
11         invoker();
12     }
13 
14 }

  运行结果如下

父类被执行

  两段运行结果都一样,这是因为父类方法输入的参数是HashMap类型,子类的输入类型参数是Map类型,也就是说子类输入参数类型的范围扩大了,子类代替父类传递到调用者中,子类的方法永远不会被执行。那如果修改一下将父类的前置条件扩大,会出现什么情况

1 public class Father {
2     public Collection doSomething(Map map){
3         System.out.println("父类被执行");
4         
5         return map.values();
6         
7     }
1 public class Son extends Father{
2     public Collection doSomething(HashMap map){
3         System.out.println("子类被执行");
4         return map.values();
5     }
6 }

  在父类的前置条件大于子类的前置条件的情况下,场景类实现如下

 1     public static void invoker(){
 2         //父类存在的地方,子类就应该能够存在
 3         Father f = new Father();
 4         HashMap map = new HashMap();
 5         f.doSomething(map);
 6     }
 7     
 8     public static void main(String[] args) {
 9         invoker();
10     }
11 
12 }

 运行结果如下

父类被执行

  引入LSP原则,将父类替换为子类

    public static void invoker(){
        //父类存在的地方,子类就应该能够存在
        Son f = new Son();
        HashMap map = new HashMap();
        f.doSomething(map);
    }
    
    public static void main(String[] args) {
        invoker();
    }

}

  运行结果如下

子类被运行

  这就出现了很严重的逻辑混乱问题。在子类没有覆写父类方法的前提下,子类被执行了,这会引起业务逻辑混乱,因为在实际应用中,父类一般都是抽象类,子类是实现类,传递这样一个实现类,就会“歪曲”父类的意图,引起一对意想不到的业务逻辑混乱,所以子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同或者更宽松。

覆写或覆写父类的方法时输出结果可以被缩小

  父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么S就必须小于T,也就是要么S和T是同一个类型,要么S是T的子类。这里分两种情况,如果是覆写,父类和子类的同名方法的输入参数是相同的,两个方法的范围值S小于等于T,这是覆写的要求。如果是重载,则要求方法的输入参数类型或数量不相同,在LSP原则下,即子类的输入参数宽于或等于父类的输入参数,也就是说这个方法不会被调用。

 

posted @ 2016-05-15 03:11  陆放为  阅读(1244)  评论(0编辑  收藏  举报