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

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

总结

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

相关推荐
七月丶9 小时前
别再手动凑 PR 了:这个 AI Skill 会按仓库习惯自动建分支、拆提交、提 PR
人工智能·设计模式·程序员
刀法如飞10 小时前
从程序员到架构师:6大编程范式全解析与实践对比
设计模式·系统架构·编程范式
九狼10 小时前
Flutter + Riverpod +MVI 架构下的现代状态管理
设计模式
静水流深_沧海一粟1 天前
04 | 别再写几十个参数的构造函数了——建造者模式
设计模式
StarkCoder1 天前
从UIKit到SwiftUI的迁移感悟:数据驱动的革命
设计模式
阿星AI工作室1 天前
给openclaw龙虾造了间像素办公室!实时看它写代码、摸鱼、修bug、写日报,太可爱了吧!
前端·人工智能·设计模式
_哆啦A梦2 天前
Vibe Coding 全栈专业名词清单|设计模式·基础篇(创建型+结构型核心名词)
前端·设计模式·vibecoding
阿闽ooo5 天前
中介者模式打造多人聊天室系统
c++·设计模式·中介者模式
小米4965 天前
js设计模式 --- 工厂模式
设计模式
逆境不可逃5 天前
【从零入门23种设计模式08】结构型之组合模式(含电商业务场景)
线性代数·算法·设计模式·职场和发展·矩阵·组合模式