SOLID原则-里式替换原则

使用面向对象的语言编写业务时,为了提高代码的复用度,会经常继承现有的类,继承也是面向对象语言的三大特性之一,其他两个是封装和多态。那有没有办法判断继承实现的合理性呢?有的,使用里式替换原则来判断继承的合理性。

如何理解里式替换原则

2008年,图灵奖授予 Barbara Liskov,表彰她在程序语言设计和系统设计方法方面的卓越工作。她在设计领域影响最深的就是以她的名字命名的 Liskov 替换原则(Liskov substitution principle,简称 LSP)。她是这么描述这条规则的:
If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program
中文翻译是:如果S是T的子类型,那么T类型的对象可以被S类型的对象替换,而不会破坏现有的程序。
在1996年,Robert Martin 在他的SOLID 原则中,重新描述了这个原则,英文为:
Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it
中文直译为:使用基类的引用的指针的函数必须可以无条件的更换使用派生类的对象。

综合两位大神的描述,我们可以简单的描述这条规则:子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变以及正确性不被破坏。

如何判断代码符合或者违反了里式替换原则

基于行为的 IS-A

IS-A ,是说,如果A是B的子类,就需要满足A是一个B(A is a B )。
有一个经典的样例经常用来解释这个原则,也就是长方形正方形问题。在数学中,我通常理解的是正方形是一种特殊的长方形。所以根据直觉,我们会写出这样的代码:

class Rectangle {
  private int height;
  private int width;
  
  // 设置长度
  public void setHeight(int height) {
    this.height = height;
  }
  
  // 设置宽度
  public void setWidth(int width) {
    this.width = width;
  }
  
  //
  public void area() {
    return this.height * this.width;
  }
}

class Square extends Rectangle {
  // 设置边长
  public void setSide(int side) {
    this.setHeight(side);
    this.setWidth(side);
t
  }
  
  @Override
  public void setHeight(int height) {
    this.setSide(height);
  }

  @Override
  public void setWidth(int width) {
    this.setSide(width);
  }
}

这段代码看上去一切很好,但是,它却是有问题的,因为它在下面的测试里会失败:

Rectangle rect = new Square();
rect.setHeight(4); // 设置长度
rect.setWidth(5);  // 设置宽度
assertThat(rect.area(), is(20)); // 对结果进行断言

在这里,Rectangle 不能被 Square 替换,这里违反了LSP的核心定义。

Design By Contract , 按照协议来设计

子类在设计的时候,要遵守父类的行为约定(协议),父类定义了函数的行为约定,子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定主要包括:函数要实现的功能;对输入、输出 、异常的约定;甚至包括注释中所罗列的任何特殊说明。
举几个例子来具体说明下

1.子类违背父类声明要实现的功能
父类中有个排序方法 order() , 按照数字降序,子类再覆写此方法时,按照数字升序排序,此时,子类的设计就违背了里式替换原则。

2.子类违背父类对输入、输出、异常的约定
在父类的某个方法约定如果没有数据时返回 empty colletion, 子类覆写时返回了 null 。
在父类的某个方法输入参数可以是任意整数,子类覆写时只允许为正整数,子类的数据校验更加严格。
在父类的某个方法约定只抛出 ArgumentNullException 异常,子类实现时抛出了其它的异常。

3.子类违背父类注释中所罗列的任何特殊说明
父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。

总结

里式替换原则用来指导继承关系中子类如何设计的一个原则。理解这一原则的核心是 IS-ADesgin by contract
里式替换原则存在的意义如下:
1、改进现有实现,例如程序原先采用低效的排序算法,改进时使用LSP原则实现更高效的排序算法
2、指导程序开发,指导我们如何组织类和子类,子类的方法要符合 contract( 协议)
3、改进抽象设计,如果一个子类的设计违反了LSP,那么要思考是不是抽象或者设计出了问题

你可能感兴趣的:(面向对象分析和设计,java,面向对象编程)