一文搞懂里氏替换原则

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

定义

里氏替换原则指出:

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

原则要点

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

  2. 预先/后置条件

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

应用场景举例

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

定义

里氏替换原则指出:

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

原则要点

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

  2. 预先/后置条件

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

应用场景举例

假设有一个抽象基类 Animal 和它的两个子类 Dog 和 Cat。如果 Animal 类有一个方法 makeSound(),并且根据动物种类发出不同的叫声。遵循LSP,Dog 和 Cat 在重写 makeSound() 方法时,必须保证调用这个方法时仍能正确执行发声行为,而不是抛出异常或执行与动物无关的动作。

// 定义一个抽象的形状基类
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对象

为何重要

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

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

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

为何重要

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

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

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

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值