设计模式-里氏替换原则(Liskov Substitution Principle, LSP)


里氏替换原则(Liskov Substitution Principle, LSP)

核心思想 :子类必须能够替换其父类,且替换后程序的正确性不受影响。
核心目标:确保继承关系的合理性,避免子类破坏父类的行为契约。


原理详解

  1. 行为兼容性

    • 子类的方法输入参数应比父类更宽松(前置条件不能更强)。
    • 子类的方法返回值应比父类更严格(后置条件不能更弱)。
    • 子类不应修改父类方法的预期行为(如抛出父类未声明的异常)。
  2. 契约设计

    • 父类定义行为规范(如接口或抽象类),子类实现需遵守这些规范。
    • 客户端代码应仅依赖父类抽象,而非具体子类实现。

应用案例

案例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 修改了 RectanglesetWidthsetHeight 的预期行为。
  • 客户端依赖 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); // 抛出异常,破坏预期行为

问题

  • CouponPaymentpay 方法抛出父类未声明的异常,客户端无法安全替换。
正确设计(遵循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 实践指南

  1. 优先组合而非继承
    通过组合和接口实现多态,避免继承带来的耦合风险。
  2. 单元测试验证
    对父类编写测试用例,确保子类通过所有父类测试。
  3. 避免重写非抽象方法
    子类不应修改父类已实现的方法逻辑。
  4. 使用设计模式
    通过 策略模式模板方法模式 等实现行为扩展。

违反 LSP 的典型场景

场景 后果 修复方案
子类抛出父类未声明的异常 客户端无法处理意外异常 子类捕获异常或父类声明通用异常
子类修改父类方法返回值 客户端逻辑错误(如空指针) 子类返回值兼容父类(更具体或相同类型)
子类强化前置条件 客户端需额外处理子类限制 父类定义更宽松的前置条件

总结

里氏替换原则是面向对象设计的基石之一,强调 子类必须无缝替换父类。通过接口定义行为契约、避免继承滥用、编写兼容性测试,可有效提升代码的健壮性和可维护性。

相关推荐
99乘法口诀万物皆可变2 小时前
C#设计模式之AbstractFactory_抽象工厂_对象创建新模式-学习
设计模式·c#·抽象工厂模式
英杰.王3 小时前
设计模式-接口隔离原则(Interface Segregation Principle, ISP)
设计模式·接口隔离原则
易元4 小时前
设计模式-状态模式
后端·设计模式
qqxhb11 小时前
零基础设计模式——总结与进阶 - 3. 学习资源与下一步
学习·设计模式·重构·代码整洁之道
我叫小白菜11 小时前
【Java_EE】设计模式
java·开发语言·设计模式
是2的10次方啊11 小时前
🎯 设计模式完全指南:从生活智慧到代码艺术
设计模式
foDol12 小时前
C++单例模式
c++·单例模式·设计模式
智想天开12 小时前
28.行为型模式分析对比
设计模式
qqxhb14 小时前
零基础设计模式——行为型模式 - 状态模式
java·设计模式·go·状态模式