欢迎来到飞鸟慕鱼博客,开始您的技术之旅!
当前位置: 首页知识笔记正文

里氏替换原则是否适用于抽象类和接口,里氏替换原则反例

墨初 知识笔记 74阅读

个人主页金鳞踏雨

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

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

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

一、原理概述

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

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

案例分析 案例一
// 基类鸟类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 cant fly);    }}

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

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

// 飞行行为接口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()。这些方法的具体实现将在子类中完成。

public abstract class Database {    public abstract void connect();    public abstract void disconnect();    public abstract void executeQuery(String query);}

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

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实现相应的方法}

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

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会导致代码变得脆弱和不易维护。

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 cant fly);    }}

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

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

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

// 栈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。

正确的写法应该是

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关系。(你就是一个它)遵循其他设计原则如单一职责原则、开闭原则和依赖倒置原则。

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

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

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

标签:
声明:无特别说明,转载请标明本文来源!