面向对象五大原则-----里氏代换原则

时间:2022-04-06 15:09:01

  什么是里氏代换原则 

  里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新

的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

  简单的理解为一个软件实体如果使用的是一个父类,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。也就是说,软件里面,把父类都替换成它的子类,程序的行为没有变化。

  但是反过来的代换却不成立,里氏代换原则(Liskov Substitution Principle):一个软件实体如果使用的是一个子类的话,那么它不能适用于其父类。

   举个例子解释一下这个概念

  先创建一个Person类

1 public class Person {
2     public void display() {
3         System.out.println("this is person");
4     }
5 }

  再创建一个Man类,继承这个Person类

1 public class Man extends Person {
2 
3     public void display() {
4         System.out.println("this is man");
5     }
6     
7 }

  运行一下

 1 public class MainClass {
 2     public static void main(String[] args) {
 3         Person person = new Person();//new一个Person实例
 4         display(person);
 5         
 6         Person man = new Man();//new一个Man实例
 7         display(man);
 8     }
 9     
10     public static void display(Person person) {
11         person.display();
12     }
13 }

  可以看到

面向对象五大原则-----里氏代换原则

  运行没有影响,符合一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类和子类对象的区别这句概念,这也就是java中的多态。

  而反之,一个子类的话,那么它不能适用于其父类,这样,程序就会报错

 1 public class MainClass {
 2     public static void main(String[] args) {
 3         Person person = new Person();
 4         display(person);//这里报错
 5         
 6         Man man = new Man();
 7         display(man);
 8     }
 9     
10     public static void display(Man man) {//传入一个子类
11         man.display();
12     }
13 }

 

  继续再举一个很经典的例子,正方形与长方形是否符合里氏代换原则,也就是说正方形是否是长方形的一个子类,

   以前,我们上学都说正方形是特殊的长方形,是宽高相等的长方形,所以我们认为正方形是长方形的子类,但真的是这样吗?

  面向对象五大原则-----里氏代换原则

  从图中,我们可以看到长方形有两个属性宽和高,而正方形则只有一个属性边长

  所以,用代码如此实现

 1 //长方形
 2 public class Changfangxing{
 3     private long width;
 4     private long height;
 5     
 6     public long getWidth() {
 7         return width;
 8     }
 9     public void setWidth(long width) {
10         this.width = width;
11     }
12     public long getHeight() {
13         return height;
14     }
15     public void setHeight(long height) {
16         this.height = height;
17     }
18 }
 1 //正方形
 2 public class Zhengfangxing{
 3     private long side;
 4 
 5     public long getSide() {
 6         return side;
 7     }
 8 
 9     public void setSide(long side) {
10         this.side = side;
11     }
12 }

 

  可以看到,它们的结构根本不同,所以正方形不是长方形的子类,所以长方形与正方形之间并不符合里氏代换原则。

  当然我们也可以强行让正方形继承长方形

 1 //正方形
 2 public class Zhengfangxing extends Changfangixng{
 3     private long side;
 4 
 5     public long getHeight() {
 6         return this.getSide();
 7     }
 8 
 9     public long getWidth() {
10         return this.getSide();
11     }
12 
13     public void setHeight(long height) {
14         this.setSide(height);
15     }
16 
17     public void setWidth(long width) {
18         this.setSide(width);
19     }
20 
21     public long getSide() {
22         return side;
23     }
24 
25     public void setSide(long side) {
26         this.side = side;
27     }
28 }

 

   这个样子,编译器是可以通过的,也可以正常使用,但是这样就符合里氏代换原则了吗,肯定不是的。

  我们不是为了继承而继承,只有真正符合继承条件的情况下我们才去继承,所以像这样为了继承而继承,强行实现继承关系的情况也是不符合里氏代换原则的。

  但这是为什么呢?,我们运行一下

 1 public class MainClass {
 2     public static void main(String[] args) {
 3         Changfangxing changfangxing = new Changfangxing();
 4         changfangxing.setHeight(10);
 5         changfangxing.setWidth(20);
 6         test(changfangxing);
 7         
 8         Changfangxing zhengfangxing = new Zhengfangxing();
 9         zhengfangxing.setHeight(10);
10         test(zhengfangxing);
11     }
12     
13     public static void test(Changfangxing changfangxing) {
14         System.out.println(changfangxing.getHeight());
15         System.out.println(changfangixng.getWidth());
16     }
17 }

  结果:

面向对象五大原则-----里氏代换原则

  我们忽然发现,很正常啊,为什么不可以,但是我们继续修改

 1 public class MainClass {
 2     public static void main(String[] args) {
 3         Changfangxing changfangxing = new Changfangxing();
 4         changfangxing.setHeight(10);
 5         changfangxing.setWidth(20);
 6         resize(changfangxing);
 7         
 8         Changfangxing zhengfangxing = new Zhengfangxing();
 9         zhengfangxing.setHeight(10);
10         resize(zhengfangxing);
11     }
12     
13     public static void test(Changfangxing changfangxing) {
14         System.out.println(changfangxing.getHeight());
15         System.out.println(changfangxing.getWidth());
16     }
17     
18     public static void resize(Changfangxing changfangxing) {
19         while(changfangxing.getHeight() <= changfangxing.getWidth()) {
20             changfangxing.setHeight(changfangxing.getHeight() + 1);
21             test(changfangxing);
22         }
23     }
24 }

  当长方形运行时,可以正常运行,而正方形则会造成死循环,所以这种继承方式不一定恩能够适用于所有情况,所以不符合里氏代换原则。

  还有一种形式,我们抽象出一个四边形接口,让长方形和正方形都实现这个接口

1 public interface Sibianxing {
2     public long getWidth();
3     public long getHeight();
4 }
 1 public class Changfangxing implements Sibianxing{
 2     private long width;
 3     private long height;
 4     
 5     public long getWidth() {
 6         return width;
 7     }
 8     public void setWidth(long width) {
 9         this.width = width;
10     }
11     public long getHeight() {
12         return height;
13     }
14     public void setHeight(long height) {
15         this.height = height;
16     }
17 }
 1 package com.ibeifeng.ex3;
 2 
 3 public class Zhengfangxing implements Sibianxing{
 4     private long side;
 5 
 6     public long getHeight() {
 7         return this.getSide();
 8     }
 9 
10     public long getWidth() {
11         return this.getSide();
12     }
13 
14     public void setHeight(long height) {
15         this.setSide(height);
16     }
17 
18     public void setWidth(long width) {
19         this.setSide(width);
20     }
21 
22     public long getSide() {
23         return side;
24     }
25 
26     public void setSide(long side) {
27         this.side = side;
28     }
29 }

  运行

 1 public class MainClass {
 2     public static void main(String[] args) {
 3         Changfangxing changfangxing = new Changfangxing();
 4         changfangxing.setHeight(10);
 5         changfangxing.setWidth(20);
 6         test(changfangxing);
 7         
 8         Zhengfangxing zhengfangxing = new Zhengfangxing();
 9         zhengfangxing.setHeight(10);
10         test(zhengfangxing);
11     }
12     
13     public static void test(Sibianxing sibianxing) {
14         System.out.println(sibianxing.getHeight());
15         System.out.println(sibianxing.getWidth());
16     }
17 }

  对于长方形和正方形,取width和height是它们共同的行为,但是给width和height赋值,两者行为不同,因此,这个抽象的四边形的类只有取值方法,没有赋值方法。上面的例子中那个方法只会适用于不同的子类,LSP也就不会被破坏。

  注意事项

  在进行设计的时候,尽量从抽象类继承,而不是从具体类继承。如果从继承等级树来看,所有叶子节点应当是具体类,而所有的树枝节点应当是抽象类或者接口。当然这个只是一个一般性的指导原则,使用的时候还要具体情况具体分析。