里氏替换原则--设计模式

1、原理概述
里式替换原则的英文翻译是:Liskov Substitution Principle,缩写为 LSP(老色
胚)。这个原则最早是在 1986 年由 Barbara Liskov 提出,他是这么描述这条原则
的:
Functions that use pointers of references to base classes must be able to
use objects of derived classes without knowing it。(使用基类引用指针的函
数必须能够在不知情的情况下使用派生类的对象。)
我们综合两者的描述,将这条原则用中文描述出来,是这样的:子类对象(object
of subtype/derived class)能够替换程序(program)中父类对象(object of
base/parent class)出现的任何地方,并且保证原来程序的逻辑行为
(behavior)不变及正确性不被破坏。


// 基类:鸟类
public class Bird {
public void fly() {
System.out.println("I can fly");
}
为了遵循LSP,我们可以重新设计类结构,将能飞的行为抽象到一个接口中,让需要
飞行能力的鸟类实现这个接口:
通过这样的设计,我们遵循了里氏替换原则,同时也保证了代码的可维护性和复用
性。
我们再来看一个基于数据库操作的案例。假设我们正在开发一个支持多种数据库的程
序,包括MySQL、PostgreSQL和SQLite。我们可以使用里氏替换原则来设计合适的
类结构,确保代码的可维护性和扩展性。
}
// 子类:企鹅类
public class Penguin extends Bird {
// 企鹅不能飞,所以覆盖了基类的fly方法,但这违反了里氏替换原则
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}

其实上这样的方法也是可以的,只是说不推荐,我们有时候也会去抛出一些相应的异常,这样的话也会产生相应的作用

在之后我们会可以去调用这个接口,这种方式是更加的方便的

// 飞行行为接口
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。我们可以使用里氏替换原则来设计合适的
类结构,确保代码的可维护性和扩展性。
}
// 子类:企鹅类
public class Penguin extends Bird {
// 企鹅不能飞,所以覆盖了基类的fly方法,但这违反了里氏替换原则
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}
// 飞行行为接口
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 {
}

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


然后,为每种数据库类型创建一个子类,继承自 Database 基类。这些子类需要实现
基类中定义的抽象方法,并可以添加特定于各自数据库的方法。
public abstract class Database {
public abstract void connect();
public abstract void disconnect();
public abstract void executeQuery(String query);
}

当我们在类中实现一个相应的控制类的时候,我们就可以去直接在这个类中进行一个方法继承

不影响其他方法的实现的过程中,我们也会去进行子类方法的新的实现

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

这里我们把这个对mysql数据库的相关的操作进行分类的实现,这样更加保证了我们的代码的可读性和复用性

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


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 基类中定义的抽象方法即可。
好了,我们稍微总结一下。虽然从定义描述和代码实现上来看,多态和里式替换有点
类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对
象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是
用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改
变原有程序的逻辑以及不破坏原有程序的正确性。

2、哪些代码明显违背了 LSP?
违背里氏替换原则(LSP)的代码通常具有以下特征:
(1)子类覆盖或修改了基类的方法
当子类覆盖或修改基类的方法时,可能导致子类无法替换基类的实例而不引起问题。
这违反了LSP,会导致代码变得脆弱和不易维护。
在这个例子中, Penguin 类覆盖了 Bird 类的 fly() 方法,抛出了一个异常。这违
反了LSP,因为现在 Penguin 实例无法替换 Bird 实例而不引发问题。
(2)子类违反了基类的约束条件
当子类违反了基类中定义的约束条件(如输入、输出或异常等),也会违反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 can't fly");
}
}
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);
}
}
// 正确的写法
public class NonNegativeStack extends Stack {
public NonNegativeStack(int size) {
super(size);
}
public void pushNonNegative(int value) {
if (value < 0) {
throw new IllegalArgumentException("Only non-negative values are
allowed");
}
super.push(value);
}
}

在这个例子中, NonNegativeStack 子类违反了 Stack 基类的约束条件,因为它在
push() 方法中添加了一个新的约束,即只允许非负数入栈。这使得
NonNegativeStack 实例无法替换 Stack 实例而不引发问题,违反了LSP。
(3)子类与基类之间缺乏"is-a"关系
当子类与基类之间缺乏真正的"is-a"关系时,也可能导致违反LSP。例如,如果一个
类继承自另一个类,仅仅因为它们具有部分相似性,而不是完全的"is-a"关系,那么
这种继承关系可能不满足LSP。
为了避免违反LSP,我们需要在设计和实现过程中注意以下几点:
1. 确保子类和基类之间存在真正的"is-a"关系。
2. 遵循其他设计原则,如单一职责原则、开闭原则和依赖倒置原则。

总结来说

LSP原则就是告诉我们对于当我们要去实现相关的方法的时候,我们不仅要去仅关注于当前方法的实现,我们还去关注对于这个方法相关的有关联的的方法,以及对于这个方法中的可以去继承的子类方法和相关调用进行继承的父类的方法

子类对象(object
of subtype/derived class)能够替换程序(program)中父类对象(object of
base/parent class)出现的任何地方,并且保证原来程序的逻辑行为
(behavior)不变及正确性不被破坏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值