一文搞懂里氏替换原则

214 阅读4分钟

里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计和编程中的核心原则之一,由芭芭拉·利斯科夫(Barbara Liskov)在1987年提出,用来指导如何合理地使用继承机制,确保程序的健壮性和可扩展性。该原则的内容可以总结为以下几点:

定义

里氏替换原则指出:

如果一个类型 T 是一个父类型,那么对于任何类型的 T 的对象 o1,都有一个类型 SS 是 T 的子类型)的对象 o2,并且在所有 T 可以出现的地方,o2 都可以替换 o1 并且程序的行为保持不变(正确性不受影响)。

原则要点

  1. 行为一致性: 子类必须完全遵循父类对外的承诺,也就是说,子类应该保留父类接口的行为。例如,如果父类有一个方法规定了某种业务规则,那么子类在重写这个方法时,不应改变或削弱这个规则,也不应引入额外的约束条件或异常情况。

  2. 预先/后置条件

    • 子类对方法的前置条件(如方法接收的参数要求)只能放宽,不能加强,也就是说,父类能接受的参数,子类也必须能够接受。
    • 子类对方法的后置条件(如返回值或内部状态的保证)可以加强,但不能减弱,意味着子类的输出至少要与父类同样可靠。
  3. 无副作用: 子类不应该在覆盖父类方法时改变全局状态,导致原本针对父类编写的代码在替换为子类对象时产生意外的结果。

应用场景举例

// 定义一个抽象的形状基类
class Shape {
  constructor(width, height) {
    if (this.constructor === Shape) {
      throw new Error("Cannot instantiate abstract class!");
    }
    this.width = width;
    this.height = height;
  }

  // 抽象方法
  getArea() { 
    throw new Error("Subclass must implement getArea method.");
  }
}

// 正方形作为Shape的子类
class Square extends Shape {
  constructor(sideLength) {
    super(sideLength, sideLength); // 正方形的宽度和高度一致
  }

  // 实现抽象方法
  getArea() {
    return this.width * this.height; // 对于正方形,面积等于边长的平方
  }
}

// 长方形作为Shape的子类
class Rectangle extends Shape {
  constructor(width, height) {
    super(width, height);
  }

  // 实现抽象方法
  getArea() {
    return this.width * this.height;
  }
}

// 使用Shape类型的引用处理不同形状
function calculateTotalArea(shapes) {
  let totalArea = 0;
  for (let shape of shapes) {
    // 这里我们期望任何一个传递进来的shape都可以计算面积
    totalArea += shape.getArea();
  }
  return totalArea;
}

// 测试
let square = new Square(5);
let rectangle = new Rectangle(3, 4);
let shapesArray = [square, rectangle];

console.log(calculateTotalArea(shapesArray)); // 输出: 49(5² + 3*4)

// 这个例子中,Square和Rectangle尽管有不同的构造方式,但是由于它们正确地实现了Shape接口(getArea方法)
// 并且在计算面积时没有改变原有的逻辑(即满足了父类的约定),因此可以无缝地替换为Shape对象

为何重要

遵循里氏替换原则的好处在于:

  • 保持封装性:子类可以自由地扩展功能,但不会改变外部对其行为的期待。
  • 促进模块化和可扩展性:允许代码依赖于抽象接口而非具体实现,这意味着在不影响现有功能的前提下,可以轻松地插入新的子类实现。
  • 减少耦合度:降低因更改子类而导致整个程序逻辑出错的可能性,从而提升系统的稳定性。
  • 支持开放封闭原则:允许对系统进行扩展,但不需要修改已有的代码。

在实际编程实践中,尤其是在设计和实现继承层级时,遵循里氏替换原则可以帮助开发者构建更加健壮、易于维护和扩展的软件系统。

为何重要

遵循里氏替换原则的好处在于:

  • 保持封装性:子类可以自由地扩展功能,但不会改变外部对其行为的期待。
  • 促进模块化和可扩展性:允许代码依赖于抽象接口而非具体实现,这意味着在不影响现有功能的前提下,可以轻松地插入新的子类实现。
  • 减少耦合度:降低因更改子类而导致整个程序逻辑出错的可能性,从而提升系统的稳定性。
  • 支持开放封闭原则:允许对系统进行扩展,但不需要修改已有的代码。

在实际编程实践中,尤其是在设计和实现继承层级时,遵循里氏替换原则可以帮助开发者构建更加健壮、易于维护和扩展的软件系统。