🌈个人主页: Aileen_0v0
🔥热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法
💫个人格言:“没有罗马,那就自己创造罗马~”
文章目录
前言
- 本文主要讲解
lsp
原则 不是你以为的下面这种lsp
, 🐶狗头保命)
lsp
原则介绍
-
里氏替换原则(LSP)
,这是SOLID原则之一。该原则由美国女计算机科学家里氏提出,强调在面向对象编程中,子类 可以替换父类的行为,但必须保持一致性。
具体来说,子类的操作结果必须与直接调用父类的操作结果一致。违反这一
原则可能导致不可预期的行为,增加代码复杂度和出错概率。- 下面我们将以银行卡中的信用卡和储蓄卡为例,介绍不遵循LSP原则的例子,以及如何通过LSP进行优化。
不遵循里氏替换原则(LSP)的例子
我们可以通过以下情境来展示如何违反里氏替换原则:
- 假设我们把 信用卡 和 储蓄卡 都直接继承自
SavingsCard
(而没有一个共同的BankCard
基类)。在这种情况下,信用卡 会有一个额外的透支功能,而 储蓄卡 则没有透支功能。这样,如果一个方法仅依赖于储蓄卡的行为(即不能透支),但我们传入了信用卡对象,程序的行为就会不符合预期,导致错误或逻辑问题。
代码实现:不遵循里氏替换原则
储蓄卡
package principles.lsp;
public class SavingsCard {
protected String cardNumber; // 卡号
protected double balance; // 余额
// 构造函数
public SavingsCard(String cardNumber, double balance) {
this.cardNumber = cardNumber;
this.balance = balance;
}
// 存款方法
public void deposit(double amount) {
if (amount <= 0) {
System.out.println("存款金额必须大于零!");
return;
}
balance += amount;
System.out.println("存款成功!当前余额:" + balance + "元");
}
// 取款方法
public void withdraw(double amount) {
if (amount <= 0) {
System.out.println("取款金额必须大于零!");
return;
}
if (amount > balance) {
System.out.println("余额不足,取款失败!");
return;
}
balance -= amount;
System.out.println("取款成功!当前余额:" + balance + "元");
}
// 查询余额
public void checkBalance() {
System.out.println("当前余额:" + balance + "元");
}
}
信用卡
package principles.lsp;
class CreditCard extends SavingsCard {
private double creditLimit; // 信用额度
// 构造函数
public CreditCard(String cardNumber, double balance, double creditLimit) {
super(cardNumber, balance);
this.creditLimit = creditLimit;
}
// 重写取款方法,允许透支
@Override
public void withdraw(double amount) {
if (amount <= 0) {
System.out.println("取款金额必须大于零!");
return;
}
if (amount > balance + creditLimit) {
System.out.println("余额不足,无法超过信用额度 " + creditLimit + " 取款!");
return;
}
balance -= amount;
System.out.println("取款成功!当前余额:" + balance + "元(信用额度剩余:" + (creditLimit + balance) + "元)");
}
}
测试类
package principles.lsp;
// 测试类
public class BankTest {
public static void main(String[] args) {
// 创建储蓄卡和信用卡对象
SavingsCard savingsCard = new SavingsCard("123456789", 1500);
CreditCard creditCard = new CreditCard("987654321", 1500, 3000);
// 储蓄卡操作
System.out.println("-------- 储蓄卡操作 --------");
savingsCard.deposit(500); // 存款
savingsCard.withdraw(1200); // 取款
savingsCard.checkBalance(); // 查询余额
// 信用卡操作
System.out.println("\n-------- 信用卡操作 --------");
creditCard.deposit(500); // 存款
creditCard.withdraw(4500); // 取款(透支)
creditCard.checkBalance(); // 查询余额
// 假设在某个地方,我们需要处理储蓄卡和信用卡,但只考虑了储蓄卡行为:
System.out.println("\n-------- 处理储蓄卡的取款 --------");
processWithdrawal(savingsCard, 1000); // 传入储蓄卡
System.out.println("\n-------- 处理信用卡的取款 --------");
processWithdrawal(creditCard, 500); // 传入信用卡
}
// 处理取款的方法,只考虑储蓄卡的行为
public static void processWithdrawal(SavingsCard card, double amount) {
System.out.println("===================");
System.out.println("处理取款:");
card.withdraw(amount); // 只期望储蓄卡的行为(没有透支功能)
}
}
运行结果及分析
在这个例子中,我们将 信用卡 直接继承 储蓄卡,并且重写了父类储蓄卡
的 withdraw()
方法的行为,导致不同的类(储蓄卡和信用卡)有了不同的行为契约,破坏了里氏替换原则(当子类信用卡作为储蓄卡时去替换它的取款行为变成可透支的了,这使得子类和父类的操作结果不一致,因为储蓄卡是不可透支的,这就不符合储蓄卡本应有的行为)。
- 因此,解决方案是确保在设计时遵循统一的行为契约,并抽象出合适的基类(如
BankCard
),避免在子类中改变父类行为的预期。这才能保证子类在父类的替换场景中保持一致性,确保程序的正确性。 - 👇️下面我将遵循
lsp
原则,将信用卡和储蓄卡的共性抽取出来,都继承自同一个父类BanCard
,使得在程序中可以 使用父类BankCard
类型 来引用它们,而程序行为仍然是正确的。
遵循lsp
的新设计
替换:
- 信用卡的
withdraw
方法和储蓄卡的withdraw
方法是不同的。储蓄卡只能在余额范围内取款,而信用卡则可以透支。 - 虽然这两个类在具体行为上有所不同,但它们都继承自同一个父类
BankCard
。这使得在程序中可以 使用父类BankCard
类型 来引用它们,而程序行为仍然是正确的。
替换的含义
- “替换” 并不是说子类的方法和父类的方法完全一样,而是子类的方法应该符合父类的接口约定,且能够在程序的其他部分被透明地替换为子类的实现。
- 子类替换父类:指的是在程序中,我们可以将父类的对象替换为子类的对象,程序依然能正确执行,而不引发错误或不一致的行为。
关键点:
- 共性抽取:父类
BankCard
定义了银行卡的共性方法(如存款、查询余额和取款)。无论是储蓄卡还是信用卡,都继承自BankCard
。 - 不影响程序行为:虽然信用卡和储蓄卡的
withdraw
方法逻辑不同,但由于它们都遵循父类BankCard
的接口规范,在程序中替换时不会引发错误。
代码修改和替换过程
父类 BankCard
BankCard
定义了银行卡的共性操作,比如存款、查询余额和取款。withdraw
作为抽象方法,这样可以明确规定所有的子类都必须提供具体的方法实现,使得设计更加的清晰。
package principles.lsp;
// 抽象类 BankCard
public abstract class BankCard {
protected String cardNumber; // 卡号
protected double balance; // 余额
// 构造函数
public BankCard(String cardNumber, double balance) {
this.cardNumber = cardNumber;
this.balance = balance;
}
// 存款方法
public void deposit(double amount) {
if (amount <= 0) {
System.out.println("存款金额必须大于零!");
return;
}
balance += amount;
System.out.println("存款成功!当前余额:" + balance + "元");
}
// 查询余额
public void checkBalance() {
System.out.println("当前余额:" + balance + "元");
}
// 抽象的取款方法,必须由子类实现
public abstract void withdraw(double amount);
}
子类 SavingsCard
(储蓄卡)
SavingsCard
继承 BankCard
,重写 withdraw
方法。
package principles.lsp;
// 储蓄卡类,继承自 BankCard
class SavingsCard extends BankCard {
public SavingsCard(String cardNumber, double balance) {
super(cardNumber, balance);
}
// 实现父类的取款方法
@Override
public void withdraw(double amount) {
if (amount <= 0) {
System.out.println("取款金额必须大于零!");
return;
}
if (amount > balance) {
System.out.println("余额不足,取款失败!");
return;
}
balance -= amount;
System.out.println("取款成功!当前余额:" + balance + "元");
}
}
子类 CreditCard
(信用卡)
CreditCard
继承 SavingsCard
,并重写了 withdraw
方法,支持透支。
package principles.lsp;
// 信用卡类,继承自 SavingsCard,并重写取款方法,支持透支
class CreditCard extends SavingsCard {
private double creditLimit; // 信用额度
public CreditCard(String cardNumber, double balance, double creditLimit) {
super(cardNumber, balance);
this.creditLimit = creditLimit;
}
// 重写取款方法,允许透支
@Override
public void withdraw(double amount) {
if (amount <= 0) {
System.out.println("取款金额必须大于零!");
return;
}
// 先检查余额,再检查信用额度
if (amount > balance + creditLimit) {
System.out.println("余额和信用额度不足,取款失败!");
return;
}
balance -= amount;
System.out.println("取款成功!当前余额:" + balance + "元(信用额度剩余:" + (creditLimit + balance) + "元)");
}
}
测试代码
package principles.lsp;
// 测试类
public class BankTest {
public static void main(String[] args) {
// 创建储蓄卡和信用卡对象
BankCard savingsCard = new SavingsCard("123456789", 1500);
BankCard creditCard = new CreditCard("987654321", 1500, 3000);
// 储蓄卡操作
System.out.println("-------- 储蓄卡操作 --------");
savingsCard.deposit(500); // 存款
savingsCard.withdraw(1200); // 取款
savingsCard.checkBalance(); // 查询余额
// 信用卡操作
System.out.println("\n-------- 信用卡操作 --------");
creditCard.deposit(500); // 存款
creditCard.withdraw(4500); // 取款(透支)
creditCard.checkBalance(); // 查询余额
// 假设在某个地方,我们需要处理储蓄卡和信用卡,但只考虑了父类 BankCard 行为:
System.out.println("\n-------- 处理银行卡的取款 --------");
processWithdrawal(savingsCard, 500); // 传入储蓄卡
System.out.println("\n-------- 处理银行卡的取款 --------");
processWithdrawal(creditCard, 500); // 传入信用卡
}
// 处理取款的方法,接受 BankCard 类型参数
public static void processWithdrawal(BankCard card, double amount) {
System.out.println("===================");
System.out.println("处理取款:");
card.withdraw(amount); // 调用卡片的 withdraw 方法
}
}
运行结果及分析:
- 通过将信用卡和储蓄卡的共性行为抽取出来统一给它们的抽象父类
BankCard
,然后通过子类去实现它们各自的方法,使得它们子类之间互不影响并且还可以替换父类的行为。
小结:
-
抽象父类
BankCard
的作用:- 提供了银行卡的统一接口,定义了通用的方法,如
deposit
、withdraw
和checkBalance
。 - 所有银行卡(如
SavingsCard
和CreditCard
)都继承自BankCard
,遵循相同的接口。
- 提供了银行卡的统一接口,定义了通用的方法,如
-
子类的行为定制:
SavingsCard
和CreditCard
各自根据自己的业务需求重写了withdraw
方法:SavingsCard
只允许在余额范围内取款;CreditCard
支持在余额加信用额度范围内取款。
-
互不影响的原因:
- 接口统一:尽管
SavingsCard
是CreditCard
的父类,它们都继承自BankCard
,并实现了各自的取款逻辑,互不干扰。 - 行为替换:可以通过
BankCard
类型的引用分别操作SavingsCard
和CreditCard
,程序可以根据具体对象执行各自的行为。 - 里氏替换原则:无论使用
SavingsCard
还是CreditCard
,它们都能正确地执行各自的逻辑,符合预期。
- 接口统一:尽管
-
替换性:
- 可以用
BankCard
类型来替换SavingsCard
或CreditCard
,它们仍然能按照各自的逻辑工作,保持行为一致性和独立性。
- 可以用
即使 SavingsCard
是 CreditCard
的父类,它们的行为通过共同的父类 BankCard
被规范化,使得不同银行卡可以互换使用,且各自的取款行为独立,不互相干扰。
总结
- 总结来说,LSP原则要求在扩展类时不修改调用方代码,确保子类对象可以替换父类对象。
-
如果我们不遵循里氏替换原则,通常会导致以下问题:
- 子类改变父类行为契约,导致使用父类类型的地方不能正确地处理子类对象,或者出现了不符合预期的行为。
- 程序逻辑依赖于特定的子类行为,而无法正常处理其他类型的子类。
-