设计模式-里氏替换原则(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 的典型场景

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

总结

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

相关推荐
Hellyc6 小时前
基于模板设计模式开发优惠券推送功能以及对过期优惠卷进行定时清理
java·数据库·设计模式·rocketmq
追烽少年x6 小时前
设计模式---观察者模式(发布-订阅模式)
网络·设计模式
秋田君6 小时前
深入理解JavaScript设计模式之命令模式
javascript·设计模式·命令模式
花好月圆春祺夏安6 小时前
基于odoo17的设计模式详解---享元模式
设计模式·享元模式
花好月圆春祺夏安8 小时前
基于odoo17的设计模式详解---命令模式
设计模式·命令模式
小飞悟12 小时前
那些年我们忽略的高频事件,正在拖垮你的页面
javascript·设计模式·面试
江上清风山间明月18 小时前
一周掌握Flutter开发--10. 结构与设计模式
flutter·设计模式·快速
牛奶咖啡1319 小时前
学习设计模式《十七》——状态模式
学习·设计模式·状态模式·认知状态模式·状态模式的优缺点·何时使用状态模式·状态模式的使用示例
找了一圈尾巴20 小时前
设计模式(行为型)-责任链模式
设计模式·责任链模式
使一颗心免于哀伤1 天前
《设计模式之禅》笔记摘录 - 5.代理模式
笔记·设计模式