跳到主要内容

里氏替换原则

什么是里氏替换原则

里氏替换原则(Liskov Substitution Principle, LSP)是由 Barbara Liskov 在1986年提出的一个重要的面向对象设计原则。其核心思想是:

如果 S 是 T 的子类型,那么在不改变程序正确性的前提下,任何 T 类型的对象都可以被 S 类型的对象所替换。

简单来说,子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

Design By Contract(按照协议设计)

里氏替换原则还有一个更具操作性的解释,即"Design By Contract"(按照协议设计)。这意味着:

  1. 子类必须完全遵守父类的行为约定
  2. 子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定
  3. 行为约定包括:
    • 函数声明要实现的功能
    • 对输入、输出、异常的约定
    • 注释中所罗列的任何特殊说明

实例说明

符合LSP的示例

public class Transporter {
protected HttpClient httpClient;

public Transporter(HttpClient httpClient) {
this.httpClient = httpClient;
}

public Response sendRequest(Request request) {
// 使用 httpClient 发送请求
return httpClient.send(request);
}
}

public class SecurityTransporter extends Transporter {
private String appId;
private String appToken;

public SecurityTransporter(HttpClient httpClient, String appId, String appToken) {
super(httpClient);
this.appId = appId;
this.appToken = appToken;
}

@Override
public Response sendRequest(Request request) {
if (appId != null && appToken != null) {
request.addPayload("app-id", appId);
request.addPayload("app-token", appToken);
}
return super.sendRequest(request);
}
}

在这个例子中,SecurityTransporter 扩展了 Transporter 的功能,但保持了原有的行为约定。

违反LSP的常见情况

1. 违背父类声明的功能

public class Parent {
public List<Order> sortOrdersByAmount(List<Order> orders) {
// 按金额排序
orders.sort((a, b) -> Double.compare(a.getAmount(), b.getAmount()));
return orders;
}
}

public class Child extends Parent {
@Override
public List<Order> sortOrdersByAmount(List<Order> orders) {
// 违反LSP:改变了函数的功能,按日期而不是金额排序
orders.sort((a, b) -> a.getCreateTime().compareTo(b.getCreateTime()));
return orders;
}
}

2. 违背输入输出约定

public class Parent {
public String process(int data) {
// 约定:出错返回null
try {
return processData(data);
} catch (Exception e) {
return null;
}
}
}

public class Child extends Parent {
@Override
public String process(int data) {
// 违反LSP:出错时抛出异常而不是返回null
if (data < 0) {
throw new IllegalArgumentException("Invalid data");
}
return processData(data);
}
}

3. 违背注释说明

public class Account {
protected double balance;

/**
* 提现操作
* 注释说明:提现金额不得超过账户余额
*/
public boolean withdraw(double amount) {
if (amount > balance) {
return false;
}
balance -= amount;
return true;
}
}

public class VIPAccount extends Account {
@Override
public boolean withdraw(double amount) {
// 违反LSP:违背了注释中关于不得超过余额的说明
balance -= amount; // 允许透支
return true;
}
}

LSP与多态的区别

虽然LSP和多态看起来相似,但它们的关注点不同:

  1. 多态是面向对象编程的一种特性,是一种代码实现的思路
  2. 里氏替换原则是一种设计原则,用于指导如何合理地设计继承关系

实践建议

  1. 在设计继承关系时,仔细思考父类的行为约定
  2. 子类可以扩展功能,但不要改变父类原有的行为约定
  3. 可以用父类的单元测试来验证子类是否符合LSP
  4. 如果发现难以遵守LSP,可能需要重新思考继承关系的设计

总结

里氏替换原则的核心是"按照协议设计",它要求:

  1. 子类必须完全遵守父类的行为约定
  2. 子类可以扩展功能,但不能改变原有的行为约定
  3. 这个原则有助于保持系统的稳定性和可维护性
  4. 违反LSP通常意味着继承关系可能需要重新设计