当前位置: 首页 > news >正文

里氏替换原则

里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计的核心原则之一,属于 SOLID 中的 L。其核心思想是:子类必须能够替换父类,且替换后程序的行为不变。简单来说,所有使用父类的地方,都应该能透明地替换成子类,而不会引发错误或意外行为。


核心规则

  1. 行为一致性:子类的方法行为应与父类一致(或更严格)。

  2. 契约不变:子类不能修改父类定义的方法参数、返回值类型或抛出的异常范围。

  3. 前置条件不加强:子类方法的输入条件不能比父类更严格。

  4. 后置条件不削弱:子类方法的输出结果不能比父类更宽松。


违反 LSP 的经典案例

案例 1:正方形继承长方形

数学中,正方形是特殊的长方形,但在编程中直接继承会违反 LSP。

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); // 同时修改宽度
    }
}

问题:当程序以父类 Rectangle 的方式使用子类 Square 时,行为会异常:

public static void testArea(Rectangle rect) {
    rect.setWidth(5);
    rect.setHeight(4);
    System.out.println("期望面积 20,实际面积:" + rect.getArea());
}

public static void main(String[] args) {
    testArea(new Rectangle()); // 输出 20(正确)
    testArea(new Square());    // 输出 16(错误!)
}

原因Square 修改了 setWidth 和 setHeight 的行为,导致结果不符合预期。

改进方案
避免让 Square 直接继承 Rectangle,而是通过接口或组合实现共性。


案例 2:企鹅继承鸟类

假设父类 Bird 有 fly() 方法,但子类企鹅不会飞:

class Bird {
    public void fly() {
        System.out.println("飞行中...");
    }
}

class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("企鹅不会飞!");
    }
}

问题:当程序调用 Bird 的 fly() 时,替换为 Penguin 会抛出异常:

public static void makeBirdFly(Bird bird) {
    bird.fly(); // 如果 bird 是企鹅,程序崩溃!
}

public static void main(String[] args) {
    makeBirdFly(new Bird());    // 正常
    makeBirdFly(new Penguin()); // 抛出异常!
}

改进方案
将飞行能力分离为接口,只有会飞的鸟才实现它:

interface Flyable {
    void fly();
}

class Sparrow implements Flyable {
    public void fly() { /* 实现飞行 */ }
}

class Penguin extends Bird { 
    // 不实现 Flyable 接口
}

符合 LSP 的案例

案例:支付系统

父类 Payment 定义支付行为,子类 CreditCardPayment 和 WeChatPayment 实现各自逻辑,但保持行为一致。

abstract class Payment {
    public abstract void pay(int amount);
}

class CreditCardPayment extends Payment {
    @Override
    public void pay(int amount) {
        System.out.println("信用卡支付:" + amount + "元");
    }
}

class WeChatPayment extends Payment {
    @Override
    public void pay(int amount) {
        System.out.println("微信支付:" + amount + "元");
    }
}

使用场景
无论替换成哪种支付方式,程序行为一致。

public static void processPayment(Payment payment, int amount) {
    payment.pay(amount);
}

public static void main(String[] args) {
    processPayment(new CreditCardPayment(), 100); // 信用卡支付
    processPayment(new WeChatPayment(), 200);     // 微信支付
}

总结

  • 里氏替换原则的本质:子类必须保持与父类的契约,确保继承是“is-a”关系(如“企鹅是鸟”不成立,但“麻雀是鸟”成立)。

  • 如何遵守 LSP

    1. 优先使用组合而非继承。

    2. 通过接口分离不同行为。

    3. 子类不重写父类非抽象方法,避免破坏原有逻辑。

我们通过具体案例逐一解释里氏替换原则(LSP)的四个核心规则,并说明如何避免违反这些规则。


1. 行为一致性:子类方法的行为应与父类一致(或更严格)

反例:子类修改父类行为
class Bird {
    // 父类方法:飞行距离为100米
    public int fly() {
        return 100;
    }
}

class Ostrich extends Bird {  // 鸵鸟不会飞
    @Override
    public int fly() {
        return 0; // 直接返回0,违反行为一致性
    }
}

问题
当程序中依赖 Bird 的 fly() 方法时,替换为 Ostrich 会导致意外结果:

void calculateDistance(Bird bird) {
    int distance = bird.fly() * 2;
    System.out.println("飞行总距离:" + distance);
}

calculateDistance(new Bird());    // 输出 200(正确)
calculateDistance(new Ostrich()); // 输出 0(错误!鸵鸟被误用为会飞的鸟)

改进方案
分离“会飞”和“不会飞”的行为,通过接口实现:

interface Flyable {
    int fly();
}

class Sparrow implements Flyable { 
    public int fly() { return 100; } 
}

class Ostrich extends Bird { 
    // 不实现 Flyable 接口
}

2. 契约不变:子类不能修改父类方法的参数、返回值或异常

反例:子类修改返回值类型
class Database {
    // 父类方法返回一个可修改的 List
    public List<String> getData() {
        return new ArrayList<>(Arrays.asList("A", "B"));
    }
}

class ReadOnlyDatabase extends Database {
    @Override
    public List<String> getData() {
        // 返回不可修改的 List(契约改变!)
        return Collections.unmodifiableList(super.getData());
    }
}

问题
父类允许修改返回的 List,但子类返回不可修改的列表,导致客户端代码崩溃:

Database db = new ReadOnlyDatabase();
db.getData().add("C"); // 抛出 UnsupportedOperationException

改进方案
保持返回值行为一致,或通过新方法明确只读特性:

class ReadOnlyDatabase extends Database {
    // 新增方法,而非重写 getData()
    public List<String> getReadOnlyData() {
        return Collections.unmodifiableList(super.getData());
    }
}

3. 前置条件不加强:子类方法的输入条件不能比父类更严格

反例:子类强化参数校验
class Account {
    // 父类允许任何金额的取款
    public void withdraw(int amount) {
        balance -= amount;
    }
}

class SafeAccount extends Account {
    @Override
    public void withdraw(int amount) {
        if (amount > balance) {  // 新增前置条件!
            throw new IllegalArgumentException("余额不足!");
        }
        super.withdraw(amount);
    }
}

问题
当父类允许透支,而子类禁止时,替换子类会破坏原有逻辑:

Account account = new SafeAccount();
account.withdraw(1000); // 如果 balance=500,抛出异常
// 但父类 Account 的设计允许透支

改进方案
如果父类允许透支,子类不应修改这一行为。可通过新类表示“安全账户”:

class SafeAccount {  // 不继承 Account
    public void safeWithdraw(int amount) {
        if (amount > balance) throw new IllegalArgumentException();
        balance -= amount;
    }
}

4. 后置条件不削弱:子类方法的输出结果不能比父类更宽松

反例:子类返回值范围扩大
class Calculator {
    // 父类保证返回值 >=0
    public int sqrt(int x) {
        if (x < 0) throw new IllegalArgumentException();
        return (int) Math.sqrt(x);
    }
}

class BuggyCalculator extends Calculator {
    @Override
    public int sqrt(int x) {
        // 未处理负数输入,可能返回负数(后置条件削弱!)
        return (int) Math.sqrt(x); 
    }
}

问题
父类保证结果非负,但子类未处理负数输入,导致客户端代码出现意外值:

Calculator calc = new BuggyCalculator();
int result = calc.sqrt(-4); // 返回 NaN 或其他未定义值(违反契约)

改进方案
子类必须维持父类的后置条件:

class BuggyCalculator extends Calculator {
    @Override
    public int sqrt(int x) {
        if (x < 0) throw new IllegalArgumentException(); // 保持后置条件
        return super.sqrt(x);
    }
}

总结

规则关键点避免方法
行为一致性子类不改变父类核心逻辑通过接口/组合替代继承
契约不变参数、返回值、异常与父类严格一致避免重写非抽象方法
前置条件不加强子类不增加输入限制子类复用父类校验逻辑
后置条件不削弱子类不放松输出约束子类覆盖时明确校验并抛出异常

核心思想:子类必须完全遵守父类的“契约”,才能透明替换父类。

问题背景

假设存在一个报表处理系统,定义了一个通用接口 CommonHandler,要求所有业务模块实现类完成请求校验(businessCheck)和业务执行(businessExecute)功能。以下是两个实现类的关键代码片段:

1. 户用模块策略类(ResidentialStrategy
@Service
public class ResidentialStrategy implements CommonHandler {
    @Override
    public void businessCheck(CommonRequest request) {
        // 校验报表类型
        if (null == ReportEnum.getDateTypeEnum(request.getReportType())) {
            throw new BusinessException("无效的报表类型");
        }
        // 新增校验:日期字段必填
        if (null == request.getDate()) {
            throw new BusinessException("日期不能为空");
        }
    }
}
2. 商用模块策略类(CommercialStrategy
@Service
public class CommercialStrategy implements CommonHandler {
    @Override
    public void businessCheck(CommonRequest request) {
        // 仅校验报表类型
        if (null == ReportEnum.getDateTypeEnum(request.getReportType())) {
            throw new BusinessException("无效的报表类型");
        }
    }
}
接口定义(CommonHandler
public interface CommonHandler {
    void businessCheck(CommonRequest request);
    CommonResponse businessExecute(CommonRequest request);
}

LSP 规则回顾:前置条件不加强

LSP 要求子类(或实现类)方法的输入条件(前置条件)不能比父类(或接口)更严格。若父类未对某个字段进行非空校验,而子类强制校验,则违反了这一规则。


问题分析

在以上代码中:

  1. 接口契约CommonHandler 的 businessCheck 方法未明确要求校验 date 字段。

  2. 实现类行为

    • CommercialStrategy 仅校验 reportType,符合接口契约。

    • ResidentialStrategy 额外校验 date 字段,单方面加强前置条件

违反 LSP 的后果

当通过 CommonHandler 接口调用 businessCheck 方法时:

  • 替换为 ResidentialStrategy 的实现时,若 date 为空,会抛出异常。

  • 替换为其他实现类(如 CommercialStrategy)时,date 为空不会报错。

结果ResidentialStrategy 无法透明替换 CommonHandler,破坏了一致性。


改进方案:分层校验设计

通过接口的默认方法(default)统一基础校验,允许子类在遵守契约的前提下扩展校验逻辑。

1. 修改接口定义
public interface CommonHandler {
    default void businessCheck(CommonRequest request) {
        // 基础校验:报表类型必填
        if (null == ReportEnum.getDateTypeEnum(request.getReportType())) {
            throw new BusinessException("无效的报表类型");
        }
    }
    CommonResponse businessExecute(CommonRequest request);
}
2. 实现类按需扩展校验
@Service
public class ResidentialStrategy implements CommonHandler {
    @Override
    public void businessCheck(CommonRequest request) {
        // 调用接口默认的基础校验
        CommonHandler.super.businessCheck(request);
        // 扩展校验:日期必填
        if (null == request.getDate()) {
            throw new BusinessException("日期不能为空");
        }
    }
}

改进后的设计解析

  1. 接口明确基础契约

    • 通过 default 方法统一实现 reportType 校验,所有实现类必须遵守。

    • 子类无需重复编写基础校验逻辑。

  2. 子类灵活扩展

    • 子类可调用 super.businessCheck() 后补充额外校验(如 date 字段)。

    • 附加校验不视为加强前置条件,因为基础契约已通过默认方法满足。

  3. 透明替换性保障

    • 所有实现类均通过 CommonHandler 接口暴露行为。

    • 调用方无需关心具体实现类的校验差异。


结论

在接口设计中遵循里氏替换原则,关键在于:

  1. 明确契约:通过默认方法或文档定义接口的基础行为约束。

  2. 分层校验:基础校验由接口统一实现,子类按需扩展而非覆盖。

  3. 契约优先:实现类的扩展逻辑不应破坏接口的原始语义。

通过这种设计,既能保证系统的行为一致性,又能灵活支持不同业务的个性化需求,最终实现高内聚、低耦合的代码结构。

相关文章:

  • SQL-查询漏洞
  • 通过国内源在Ubuntu20.0.4安装repo
  • 【时时三省】(C语言基础)用if语句实现选择结构
  • 多层感知机实现
  • Qt 线程类
  • 在普通用户下修改root用户密码
  • python中闭包与装饰器
  • DeepSeek助力Vue开发:打造丝滑的键盘快捷键
  • 使用 LLaMA-Factory 微调 llama3 模型
  • LeetCode热题100JS(74/100)第十四天|155|394|739|84|215
  • 【网络安全 | 漏洞挖掘】绕过管理员权限撤销的访问控制漏洞
  • Walrus 基金会完成 1.4 亿美元融资,由 Standard Crypto 领投
  • aab 转 apk
  • 笔试面试01 c/c++
  • 菜鸟的程序编程理解
  • PHP大马的使用
  • 【Spiffo】光速项目:LVGL v9框架下的MIPI简易相机_Part1
  • [数据结构]1.时间复杂度和空间复杂度
  • Resume全栈项目(二)(.React+Ts)
  • AI知识补全(四):微调 Fine-tuning 是什么?
  • 消防有哪些网站合适做/外链网盘
  • 国家企业信用查询系统官网/定西seo排名
  • 做网站有哪些软件/成人技能培训机构
  • 怎么给制作网站谷歌地图/甲马营seo网站优化的
  • 沈阳网站建设推广/seo搜索优化服务
  • 网站设计知识准备/新郑网络推广公司