【23种设计模式】里氏替换原则

个人主页金鳞踏雨

个人简介 :大家好,我是金鳞,一个初出茅庐的Java小白

目前状况:22届普通本科毕业生,几经波折了,现在任职于一家国内大型知名日化公司,从事Java开发工作

我的博客:这里是CSDN,是我学习技术,总结知识的地方。希望和各位大佬交流,共同进步 ~

本文来自抖音**《IT楠老师》**设计模式课程,下面是本人结合原课件的一些学习心得。

一、原理概述

子类对象 能够替换程序中父类对象出现的任何地方,并且保证原来程序的 逻辑行为不变 正确性不被破坏

举一个最简单的例子,就是我们定义了一个函数,它的参数是 XXX(List list),当我们传入ArrayList的对象(它的子类),不会有任何问题!

案例分析

案例一

java 复制代码
// 基类:鸟类
public class Bird {
    public void fly() {
        System.out.println("I can fly");
    }
}

// 子类:企鹅类
public class Penguin extends Bird {
    // 企鹅不能飞,所以覆盖了基类的fly方法,但这违反了里氏替换原则
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly");
    }
}

企鹅不会飞,所以在企鹅类中重写的fly(),会抛出异常。那么这个子类并不能很好的替代父类,因为它执行fly() 方法会报错!!!

为了遵循LSP,我们可以重新设计类结构,将能飞的行为抽象到一个接口中,让需要飞行能力的鸟类实现这个接口,对于企鹅,就不实现这个方法

java 复制代码
// 飞行行为接口
public interface Flyable {
    void fly();
}

// 基类:鸟类
public class Bird {
}

// 子类:能飞的鸟类
public class FlyingBird extends Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("I can fly");
    }
}

// 子类:企鹅类,不实现Flyable接口!!!
public class Penguin extends Bird {
}

通过这样的设计,我们遵循了里氏替换原则,同时也保证了代码的可维护性和复用性。

案例二

假设我们正在开发一个支持多种数据库的程序,包括MySQL、PostgreSQL和SQLite 。我们可以使用里氏替换原则来设计合适的类结构,确保代码的可维护性和扩展性。

首先,我们定义一个抽象的Database基类,它包含一些通用的数据库操作方法,如connect()、disconnect()和executeQuery()。这些方法的具体实现将在子类中完成。

java 复制代码
public abstract class Database {
    public abstract void connect();
    public abstract void disconnect();
    public abstract void executeQuery(String query);
}

然后,为每种数据库类型创建一个子类,继承自Database基类。这些子类需要实现基类中定义的抽象方法,并可以添加特定于各自数据库的方法。

java 复制代码
public class MySQLDatabase extends Database {
    @Override
    public void connect() {
        // 实现MySQL的连接逻辑
    }

    @Override
    public void disconnect() {
        // 实现MySQL的断开连接逻辑
    }

    @Override
    public void executeQuery(String query) {
        // 实现MySQL的查询逻辑
    }

    // 其他针对MySQL的特定方法
}

public class PostgreSQLDatabase extends Database {
    // 类似地,为PostgreSQL实现相应的方法
}

public class SQLiteDatabase extends Database {
    // 类似地,为SQLite实现相应的方法
}

这样设计的好处是,我们可以在不同的数据库类型之间灵活切换,而不需要修改大量代码。只要这些子类遵循里氏替换原则,我们就可以放心地使用基类的引用来操作不同类型的数据库。例如:

java 复制代码
public class DatabaseClient {
    private Database database;

    public DatabaseClient(Database database) {
        this.database = database;
    }

    public void performDatabaseOperations() {
        database.connect();
        database.executeQuery("SELECT * FROM users");
        database.disconnect();
    }
}

public class Main {
    public static void main(String[] args) {
        // 使用MySQL数据库
        DatabaseClient client1 = new DatabaseClient(new MySQLDatabase());
        client1.performDatabaseOperations();

        // 切换到PostgreSQL数据库
        DatabaseClient client2 = new DatabaseClient(new PostgreSQLDatabase());
        client2.performDatabaseOperations();

        // 切换到SQLite数据库
        DatabaseClient client3 = new DatabaseClient(new SQLiteDatabase());
        client3.performDatabaseOperations();
    }
}

通过遵循里氏替换原则,我们确保了代码的可维护性和扩展性。如果需要支持新的数据库类型,只需创建一个新的子类,实现Database基类中定义的抽象方法即可。

好了,我们稍微总结一下。虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。

  • 多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。
  • 里式替换是一种设计原则,是用来指导 继承关系中子类该如何设计****的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

二、哪些代码明显违背了 LSP?

1. 子类覆盖或修改了基类的方法

当子类覆盖或修改基类的方法时,可能导致子类无法替换基类的实例而不引起问题。这违反了LSP,会导致代码变得脆弱和不易维护。

java 复制代码
public class Bird {
    public void fly() {
        System.out.println("I can fly");
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly");
    }
}

在这个例子中,Penguin类覆盖了Bird类的fly()方法,抛出了一个异常 。这违反了LSP ,因为现在Penguin实例无法替换Bird实例

2. 子类违反了基类的约束条件

当子类违反了基类中定义的约束条件(如输入、输出或异常等),也会违反LSP。

java 复制代码
// 栈
public class Stack {
    private int top;
    private int[] elements;

    public Stack(int size) {
        elements = new int[size];
        top = -1;
    }

    public void push(int value) {
        if (top >= elements.length - 1) {
            throw new IllegalStateException("Stack is full");
        }
        elements[++top] = value;
    }

    public int pop() {
        if (top < 0) {
            throw new IllegalStateException("Stack is empty");
        }
        return elements[top--];
    }
}

// 正数的栈
public class NonNegativeStack extends Stack {
    public NonNegativeStack(int size) {
        super(size);
    }

    @Override
    public void push(int value) {
        if (value < 0) {
            throw new IllegalArgumentException("Only non-negative values are allowed");
        }
        super.push(value);
    }
}

在这个例子中,NonNegativeStack 子类违反了Stack基类的约束条件,因为它在push()方法添加了一个新的约束 ,即只允许非负数入栈。这使得NonNegativeStack实例无法替换Stack实例而不引发问题,违反了LSP。

正确的写法应该是:

java 复制代码
public class NonNegativeStack extends Stack {
    public NonNegativeStack(int size) {
        super(size);
    }

    @Override
    public void push(int value) {
        super.push(value);
    }

    // 定义新的约束条件
    public void pushNonNegative(int value) {
        if (value < 0) {
            throw new IllegalArgumentException("Only non-negative values are allowed");
        }
        super.push(value);
    }
}

3. 子类与基类之间缺乏"is-a"关系

子类基类 之间缺乏真正的**"is-a"关系**时,也可能导致违反LSP。例如,如果一个类继承自另一个类,仅仅因为它们具有部分相似性,而不是完全的"is-a"关系,那么这种继承关系可能不满足LSP。

为了避免违反LSP,我们需要在设计和实现过程中注意以下几点:

  • 确保子类和基类之间存在真正的"is-a" 关系。(你就是一个它)
  • 遵循其他设计原则,如单一职责原则、开闭原则和依赖倒置原则。

文章到这里就结束了,如果有什么疑问的地方,可以在评论区指出~

希望能和大佬们一起努力,诸君顶峰相见

再次感谢各位小伙伴儿们的支持!!!

相关推荐
懂得节能嘛.1 分钟前
【动态配置中心】Java+Redis构建动态配置中心
java·开发语言·redis
专注于大数据技术栈2 分钟前
Java中JDK、JRE、JVM概念
java·开发语言·jvm
YuanlongWang6 分钟前
C# 基础——值类型与引用类型的本质区别
java·jvm·c#
Kay_Liang30 分钟前
大语言模型如何精准调用函数—— Function Calling 系统笔记
java·大数据·spring boot·笔记·ai·langchain·tools
自由的疯1 小时前
Java 如何学习Docker
java·后端·架构
自由的疯1 小时前
Java Docker本地部署
java·后端·架构
007php0071 小时前
猿辅导Java面试真实经历与深度总结(二)
java·开发语言·python·计算机网络·面试·职场和发展·golang
摇滚侠1 小时前
Spring Boot 3零基础教程,WEB 开发 内容协商机制 笔记34
java·spring boot·笔记·缓存
一勺菠萝丶1 小时前
在 macOS 上用 Docker 为 Java 后端 & 常见开发需求搭建完整服务(详尽教程)
java·macos·docker
顾漂亮1 小时前
JVM底层攻坚
java·jvm·spring