文章目录
- 一、定义:里氏替换原则
-
- [1.1 里氏替换原则](#1.1 里氏替换原则)
- [1.2 里氏替换原则的作用](#1.2 里氏替换原则的作用)
- 二、模拟场景:里氏替换原则
- 三、违背方案:里氏替换原则
-
- [3.1 工程结构](#3.1 工程结构)
- [3.2 储蓄卡和信用卡](#3.2 储蓄卡和信用卡)
-
- [3.2.1 储蓄卡](#3.2.1 储蓄卡)
- [3.2.2 信用卡](#3.2.2 信用卡)
- [3.3 单元测试](#3.3 单元测试)
-
- [3.3.1 储蓄卡测试](#3.3.1 储蓄卡测试)
- [3.3.2 信用卡测试](#3.3.2 信用卡测试)
- 四、改善代码:里氏替换原则
-
- [4.1 工程结构](#4.1 工程结构)
- [4.2 银行卡:储蓄卡和信用卡](#4.2 银行卡:储蓄卡和信用卡)
-
- [4.2.1 抽象银行卡类](#4.2.1 抽象银行卡类)
- [4.2.2 储蓄卡](#4.2.2 储蓄卡)
- [4.2.2 信用卡](#4.2.2 信用卡)
- [4.3 单元测试](#4.3 单元测试)
-
- [4.3.1 里氏替换测试](#4.3.1 里氏替换测试)
- [4.3.2 信用卡测试](#4.3.2 信用卡测试)
- 五、总结:里氏替换原则
一、定义:里氏替换原则
1.1 里氏替换原则
- 里氏替换原则 :
Liskov Substitution Principle,LSP
。- 如果 S 是 T 的子类型,那么所有 T 类型的对象都可以在不破坏程序的情况下被 S 类型的对象替换。
- 简单来说:子类可以扩展父类的功能,但不能改变父类原有的功能。
- 也就是说:当子类继承父类时,除添加新的方法且完成新增功能外,尽量不要重写父类的方法 。
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松。
- 当子类的方法实现父类的方法(重写、重载或实现抽象方法)时,方法的后置条件(即方法的输出或返回值)要比父类的方法更严格或与父类的方法相等。
1.2 里氏替换原则的作用
- 里氏替换原则是实现开闭原则的重要方式之一。
- 解决了继承中重写父类造成的可复用性变差的问题。
- 是动作正确性的保证,即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
- 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。
二、模拟场景:里氏替换原则
- 我们会使用各种类型的银行卡,例如储蓄卡、信用卡,还有一些其他特性的银行卡。
- 储蓄卡和信用卡都具备一定的消费功能,但又有一些不同。例如信用卡不宜提现,如果提现可能会产生高额的利息。
- 模拟场景:假设在构建银行系统时,储蓄卡是第一个类,信用卡是第二个类。
- 为了让信用卡可以使用储蓄卡的一些方法,选择由信用卡类继承储蓄卡类,讨论是否满足里氏替换原则产生的一些要点。
三、违背方案:里氏替换原则
- 储蓄卡和信用卡在使用功能上类似,都有支付、提现、还款、充值等功能,但有些许不同。
- 例如支付:储蓄卡做的是账户扣款动作,信用卡做的是生成贷款单动作。
3.1 工程结构
jsx
design-1.3-0
|------src
|------main
|--java
|--com.lino.design
|--CashCard.java
|--CreditCard.java
|------test
|--java
|--com.lino.design.test
|--ApiTest.java
3.2 储蓄卡和信用卡
3.2.1 储蓄卡
CashCard.java
java
package com.lino.design;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 模拟储蓄卡功能
*/
public class CashCard {
private Logger logger = LoggerFactory.getLogger(CashCard.class);
/**
* 提现
*
* @param orderId 单号
* @param amount 金额
* @return 状态码 0000成功、0001失败、0002重复
*/
public String withdrawal(String orderId, BigDecimal amount) {
// 模拟支付成功
logger.info("提现成功,单号:{} 金额:{}", orderId, amount);
return "0000";
}
/**
* 储值
*
* @param orderId 单号
* @param amount 金额
* @return 状态码 0000成功、0001失败、0002重复
*/
public String recharge(String orderId, BigDecimal amount) {
// 模拟充值成功
logger.info("储值成功,单号:{} 金额:{}", orderId, amount);
return "0000";
}
/**
* 交易流水查询
*
* @return 交易流水
*/
public List<String> tradeFlow() {
logger.info("交易流水查询成功");
List<String> tradeList = new ArrayList<>();
tradeList.add("100001,100.00");
tradeList.add("100001,80.00");
tradeList.add("100001,76.50");
tradeList.add("100001,126.00");
return tradeList;
}
}
- 在储蓄卡的功能实现中包括了三个方法:提现、储蓄、交易流水查询,这些是模拟储蓄卡的基本功能。
3.2.2 信用卡
CreditCard.java
java
package com.lino.design;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.util.List;
/**
* @description: 模拟信用卡功能
*/
public class CreditCard extends CashCard {
private Logger logger = LoggerFactory.getLogger(CreditCard.class);
@Override
public String withdrawal(String orderId, BigDecimal amount) {
// 校验
if (amount.compareTo(new BigDecimal(1000)) >= 0) {
logger.info("贷款金额校验(限额1000元),单号:{} 金额:{}", orderId, amount);
return "0001";
}
// 模拟生成贷款单
logger.info("生成贷款单,单号:{} 金额:{}", orderId, amount);
// 模拟支付成功
logger.info("贷款成功,单号:{} 金额:{}", orderId, amount);
return "0000";
}
@Override
public String recharge(String orderId, BigDecimal amount) {
// 模拟生成还款单
logger.info("生成还款单,单号:{} 金额:{}", orderId, amount);
// 模拟还款成功
logger.info("还款成功,单号:{} 金额:{}", orderId, amount);
return "0000";
}
@Override
public List<String> tradeFlow() {
return super.tradeFlow();
}
}
- 信用卡的功能实现是在继承了储蓄卡类后,进行方法重学:支付
withdrawal()
、还款recharge()
。交易流水可以复用,不用重写这个类。 - 这种继承父类方式的优点是复用了父类的核心功能逻辑,但是也破坏了原有的方法。
- 此时继承父类实现的信用卡类并不满足里氏替换原则,也就是说,此时的子类不能承担原父类的功能,直接给储蓄卡使用。
3.3 单元测试
3.3.1 储蓄卡测试
ApiTest.java
java
@Test
public void test_CashCard() {
CashCard cashCard = new CashCard();
// 提现
cashCard.withdrawal("100001", new BigDecimal(100));
// 储蓄
cashCard.recharge("100001", new BigDecimal(100));
// 交易流水
List<String> tradeFlow = cashCard.tradeFlow();
logger.info("查询交易流水:{}", JSON.toJSONString(tradeFlow));
}
测试结果
java
10:58:28.027 [main] INFO com.lino.design.CashCard - 提现成功,单号:100001 金额:100
10:58:28.031 [main] INFO com.lino.design.CashCard - 储值成功,单号:100001 金额:100
10:58:28.031 [main] INFO com.lino.design.CashCard - 交易流水查询成功
10:58:28.169 [main] INFO com.lino.design.test.ApiTest - 查询交易流水:["100001,100.00","100001,80.00","100001,76.50","100001,126.00"]
3.3.2 信用卡测试
ApiTest.java
java
@Test
public void test_CreditCard() {
CreditCard creditCard = new CreditCard();
// 支付
creditCard.withdrawal("100001", new BigDecimal(100));
// 还款
creditCard.recharge("100001", new BigDecimal(100));
// 交易流水
List<String> tradeFlow = creditCard.tradeFlow();
logger.info("查询交易流水:{}", JSON.toJSONString(tradeFlow));
}
测试结果
10:59:23.970 [main] INFO com.lino.design.CreditCard - 生成贷款单,单号:100001 金额:100
10:59:23.970 [main] INFO com.lino.design.CreditCard - 贷款成功,单号:100001 金额:100
10:59:23.970 [main] INFO com.lino.design.CreditCard - 生成还款单,单号:100001 金额:100
10:59:23.970 [main] INFO com.lino.design.CreditCard - 还款成功,单号:100001 金额:100
10:59:23.970 [main] INFO com.lino.design.CashCard - 交易流水查询成功
10:59:24.003 [main] INFO com.lino.design.test.ApiTest - 查询交易流水:["100001,100.00","100001,80.00","100001,76.50","100001,126.00"]
四、改善代码:里氏替换原则
4.1 工程结构
jsx
design-1.3-1
|------src
|------main
|--java
|--com.lino.design
|--BandCard.java
|--CashCard.java
|--CreditCard.java
|------test
|--java
|--com.lino.design.test
|--ApiTest.java
4.2 银行卡:储蓄卡和信用卡
- 储蓄卡和信用卡在功能使用上有些许类似,在实际的开发过程中也有很多共同可复用的属性及逻辑。
- 实现这样的类的最好方式是提取出一个抽象类,由抽象类定义所有卡的共用核心属性、逻辑,把卡的支付和还款等动作抽象成正向和逆向操作。
4.2.1 抽象银行卡类
BandCard.java
java
package com.lino.design;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 银行卡
*/
public abstract class BandCard {
private Logger logger = LoggerFactory.getLogger(BandCard.class);
/**
* 卡号
*/
private String cardNo;
/**
* 开卡时间
*/
private String cardDate;
public BandCard(String cardNo, String cardDate) {
this.cardNo = cardNo;
this.cardDate = cardDate;
}
/**
* 金额判断规则
*
* @param amount 金额
* @return 是否符合规则
*/
abstract boolean rule(BigDecimal amount);
/**
* 正向入账:+钱
*
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public String positive(String orderId, BigDecimal amount) {
// 入款成功,存款、还款
logger.info("卡号{} 入款成功:单号:{} 金额:{}", cardNo, orderId, amount);
return "0000";
}
/**
* 逆向入账:-钱
*
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public String negative(String orderId, BigDecimal amount) {
// 出款成功,支付、贷款
logger.info("卡号{} 出款成功:单号:{} 金额:{}", cardNo, orderId, amount);
return "0000";
}
/**
* 交易流水查询
*
* @return 交易流水
*/
public List<String> tradeFlow() {
logger.info("交易流水查询成功");
List<String> tradeList = new ArrayList<>();
tradeList.add("100001,100.00");
tradeList.add("100001,80.00");
tradeList.add("100001,76.50");
tradeList.add("100001,126.00");
return tradeList;
}
public String getCardNo() {
return cardNo;
}
public String getCardDate() {
return cardDate;
}
}
- 在抽象银行卡类中,提供了基本的卡属性,包括卡号、开卡时间及三个核心方法。
- 正向入账,加钱;逆向入账,减钱;交易流水查询;
4.2.2 储蓄卡
CashCard.java
java
package com.lino.design;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
/**
* @description: 模拟储值卡功能
*/
public class CashCard extends BandCard {
private Logger logger = LoggerFactory.getLogger(CashCard.class);
public CashCard(String cardNo, String cardDate) {
super(cardNo, cardDate);
}
@Override
boolean rule(BigDecimal amount) {
return true;
}
/**
* 提现
*
* @param orderId 单号
* @param amount 金额
* @return 状态码 0000成功、0001失败、0002重复
*/
public String withdrawal(String orderId, BigDecimal amount) {
// 模拟支付成功
logger.info("提现成功,单号:{} 金额:{}", orderId, amount);
return super.negative(orderId, amount);
}
/**
* 储值
*
* @param orderId 单号
* @param amount 金额
* @return 状态码 0000成功、0001失败、0002重复
*/
public String recharge(String orderId, BigDecimal amount) {
// 模拟充值成功
logger.info("储值成功,单号:{} 金额:{}", orderId, amount);
return super.positive(orderId, amount);
}
/**
* 风险校验
*
* @param cardNo 卡号
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public boolean checkRisk(String cardNo, String orderId, BigDecimal amount) {
// 模拟风控校验
logger.info("风控校验:卡号:{} 单号:{} 金额:{}", cardNo, orderId, amount);
return true;
}
}
- 储蓄卡类中继承抽象银行卡父类
BandCard
,实现的核心功能包括规则过滤rule
、提现withdrawal
、储蓄recharge
和新增的扩展方法,即风控校验checkRisk
。 - 这样的实现方式满足了里氏替换的基本原则,即实现抽象类的抽象方法,又没有破坏父类中的原有方法。
4.2.2 信用卡
CreditCard.java
java
package com.lino.design;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
/**
* @description: 信用卡
*/
public class CreditCard extends CashCard {
private Logger logger = LoggerFactory.getLogger(CreditCard.class);
public CreditCard(String cardNo, String cardDate) {
super(cardNo, cardDate);
}
boolean rule2(BigDecimal amount) {
return amount.compareTo(new BigDecimal(1000)) <= 0;
}
/**
* 提现,信用卡贷款
*
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public String loan(String orderId, BigDecimal amount) {
boolean rule = rule2(amount);
// 校验
if (!rule) {
logger.info("生成贷款单失败,金额超限。单号:{} 金额:{}", orderId, amount);
return "0001";
}
// 模拟生成贷款单
logger.info("生成贷款单,单号:{} 金额:{}", orderId, amount);
// 模拟支付成功
logger.info("贷款成功,单号:{} 金额:{}", orderId, amount);
return super.negative(orderId, amount);
}
/**
* 还款,信用卡还款
*
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public String repayment(String orderId, BigDecimal amount) {
// 模拟生成还款单
logger.info("生成还款单,单号:{} 金额:{}", orderId, amount);
// 模拟还款成功
logger.info("还款成功,单号:{} 金额:{}", orderId, amount);
return super.positive(orderId, amount);
}
}
- 信用卡类在继承父类后,使用了公用的属性,即卡号
cardNo
、开卡时间cardDate
,同时新增了符合信用卡功能的新方法,即贷款loan
、还款repayment
,并在两个方法中都使用了抽象类的核心功能。 - 另外,关于储蓄卡中的规则校验方法,新增了自己的规则方法
rule2
,并没有破坏储蓄卡中的校验方法。 - 以上的实现方式都是在遵循里氏替换原则下完成的,子类随时可以替换储蓄卡类。
4.3 单元测试
4.3.1 里氏替换测试
ApiTest.java
java
@Test
public void test_bandCard() {
logger.info("里氏替换前,CashCard类:");
CashCard bandCard = new CashCard("6214567800989876", "2022-12-14");
// 提现
bandCard.withdrawal("100001", new BigDecimal(100));
// 储蓄
bandCard.recharge("100001", new BigDecimal(100));
logger.info("里氏替换后,CreditCard类:");
CashCard creditCard = new CreditCard("6214567800989876", "2022-12-14");
// 提现
creditCard.withdrawal("100001", new BigDecimal(1000000));
// 储蓄
creditCard.recharge("100001", new BigDecimal(100));
}
测试结果
11:16:03.817 [main] INFO com.lino.design.test.ApiTest - 里氏替换前,CashCard类:
11:16:03.817 [main] INFO com.lino.design.CashCard - 提现成功,单号:100001 金额:100
11:16:03.817 [main] INFO com.lino.design.BandCard - 卡号6214567800989876 出款成功:单号:100001 金额:100
11:16:03.817 [main] INFO com.lino.design.CashCard - 储值成功,单号:100001 金额:100
11:16:03.817 [main] INFO com.lino.design.BandCard - 卡号6214567800989876 入款成功:单号:100001 金额:100
11:16:03.817 [main] INFO com.lino.design.test.ApiTest - 里氏替换后,CreditCard类:
11:16:03.817 [main] INFO com.lino.design.CashCard - 提现成功,单号:100001 金额:1000000
11:16:03.817 [main] INFO com.lino.design.BandCard - 卡号6214567800989876 出款成功:单号:100001 金额:1000000
11:16:03.817 [main] INFO com.lino.design.CashCard - 储值成功,单号:100001 金额:100
11:16:03.817 [main] INFO com.lino.design.BandCard - 卡号6214567800989876 入款成功:单号:100001 金额:100
4.3.2 信用卡测试
ApiTest.java
@Test
public void test_CreditCard() {
CreditCard creditCard = new CreditCard("6214567800989876", "2022-12-14");
// 支付,贷款
creditCard.loan("100001", new BigDecimal(100));
// 还款
creditCard.repayment("100001", new BigDecimal(100));
}
测试结果
11:13:03.042 [main] INFO com.lino.design.CreditCard - 生成贷款单,单号:100001 金额:100
11:13:03.042 [main] INFO com.lino.design.CreditCard - 贷款成功,单号:100001 金额:100
11:13:03.042 [main] INFO com.lino.design.BandCard - 卡号6214567800989876 出款成功:单号:100001 金额:100
11:13:03.042 [main] INFO com.lino.design.CreditCard - 生成还款单,单号:100001 金额:100
11:13:03.042 [main] INFO com.lino.design.CreditCard - 还款成功,单号:100001 金额:100
11:13:03.042 [main] INFO com.lino.design.BandCard - 卡号6214567800989876 入款成功:单号:100001 金额:100
- 通过以上的测试结果可以看到,储蓄卡功能正常,继承储蓄卡实现的信用卡功能也正常。
- 同时,原有储蓄卡类的功能可以由信用卡类支持,即
CashCard creditCard = new CreditCard()
。
五、总结:里氏替换原则
- 继承作为面向对象的重要特征,虽然给程序开发带来了非常大的便利,但也引入了一些弊端。
- 继承的开发方式会给代码带来侵入性,可移植能力降低,类之间的耦合度较高。
- 当对父类修改时,就要考虑一整套子类的实现是否由风险,测试成本较高。
- 里氏替换原则的目的是使用约定的方式,让使用继承后的代码具备良好的扩展性和兼容性。
- 在日常开发中使用继承的地方并不多,在代码规范中也不会允许多层继承,尤其是一些核心服务的扩展。
- 而继承多数用在系统架构初期定义好的逻辑上或抽象出的核心功能里。
- 如果使用了继承,就一定要遵从里氏替换原则,否则会让代码出现问题的概率变得更大。