里氏替换原则(Liskov Substitution Principle, LSP)
核心思想 :子类必须能够替换其父类,且替换后程序的正确性不受影响。
核心目标:确保继承关系的合理性,避免子类破坏父类的行为契约。
原理详解
-
行为兼容性
- 子类的方法输入参数应比父类更宽松(前置条件不能更强)。
- 子类的方法返回值应比父类更严格(后置条件不能更弱)。
- 子类不应修改父类方法的预期行为(如抛出父类未声明的异常)。
-
契约设计
- 父类定义行为规范(如接口或抽象类),子类实现需遵守这些规范。
- 客户端代码应仅依赖父类抽象,而非具体子类实现。
应用案例
案例1:几何图形计算(经典反例)
错误设计(违反LSP)
java
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width); // 强制宽高相等,破坏父类行为
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height); // 同上
}
}
// 客户端代码
public void calculateArea(Rectangle rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(4);
System.out.println(rectangle.getArea()); // 预期 20,但传入 Square 时输出 16
}
问题:
Square
修改了Rectangle
的setWidth
和setHeight
的预期行为。- 客户端依赖
Rectangle
的规范,但Square
破坏了该规范。
正确设计(遵循LSP)
java
interface Shape {
int getArea();
}
class Rectangle implements Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() { return width * height; }
}
class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() { return side * side; }
}
// 客户端代码
public void calculateArea(Shape shape) {
System.out.println(shape.getArea());
}
优势:
- 通过接口
Shape
定义统一契约,子类独立实现。 - 客户端无需关心具体类型,仅依赖抽象。
案例2:支付系统设计
错误设计(违反LSP)
java
abstract class Payment {
public abstract void pay(double amount);
}
class CreditCardPayment extends Payment {
@Override
public void pay(double amount) { /* 信用卡支付逻辑 */ }
}
class CouponPayment extends Payment {
@Override
public void pay(double amount) {
throw new UnsupportedOperationException("优惠券支付不支持金额修改"); // 违反父类契约
}
}
// 客户端代码
Payment payment = new CouponPayment();
payment.pay(100); // 抛出异常,破坏预期行为
问题:
CouponPayment
的pay
方法抛出父类未声明的异常,客户端无法安全替换。
正确设计(遵循LSP)
java
interface Payable {
void pay(double amount) throws PaymentException; // 明确声明可能异常
}
class CreditCardPayment implements Payable {
@Override
public void pay(double amount) { /* 正常支付逻辑 */ }
}
class CouponPayment implements Payable {
@Override
public void pay(double amount) throws PaymentException {
if (amount != 0) {
throw new PaymentException("优惠券金额不可修改");
}
// 使用固定金额支付
}
}
// 客户端代码
public void processPayment(Payable payment, double amount) {
try {
payment.pay(amount);
} catch (PaymentException e) {
// 统一处理异常
}
}
优势:
- 子类
CouponPayment
明确声明异常,客户端可预期并处理。 - 所有实现类均遵守
Payable
接口的契约。
LSP 实践指南
- 优先组合而非继承 :
通过组合和接口实现多态,避免继承带来的耦合风险。 - 单元测试验证 :
对父类编写测试用例,确保子类通过所有父类测试。 - 避免重写非抽象方法 :
子类不应修改父类已实现的方法逻辑。 - 使用设计模式 :
通过 策略模式 、模板方法模式 等实现行为扩展。
违反 LSP 的典型场景
场景 | 后果 | 修复方案 |
---|---|---|
子类抛出父类未声明的异常 | 客户端无法处理意外异常 | 子类捕获异常或父类声明通用异常 |
子类修改父类方法返回值 | 客户端逻辑错误(如空指针) | 子类返回值兼容父类(更具体或相同类型) |
子类强化前置条件 | 客户端需额外处理子类限制 | 父类定义更宽松的前置条件 |
总结
里氏替换原则是面向对象设计的基石之一,强调 子类必须无缝替换父类。通过接口定义行为契约、避免继承滥用、编写兼容性测试,可有效提升代码的健壮性和可维护性。