里氏替换原则
1.1 里氏替换原则定义
里氏替换原则由麻省理工学院计算机系教授芭芭拉•利斯科夫(Barbara Liskov)提出,她提出:继承必须保证超类所拥有的特性在子类中任然成立。
1.2 里氏替换原则
如果S是T的子类,那么S类型的对象在不破换程序的情况下都应该可以替换T类型的对象。
简单来说,就是子类可以扩展父类的功能,但是不能改变父类原有的功能。也就是说,子类在完成继承父类是除了添加新的方法和完成新的功能外,尽量不要重写父类非抽象方法,这句话的意思是:
- 子类可以实现父类抽象方法,但不能覆盖父类非抽象方法
- 子类可以新增自己特有的方法
- 当子类重载父类方法时,方法的输入参数要比父类方法更宽松
- 当子类的方法实现父类的方法(重写,重载或实现抽象方法)时,方法的输出参数要比父类的方法更严格或与父类的方法相等。
1.3 里氏替换原则的作用
- 里氏替换原则是实现开闭原则的总要方式之一。
- 解决了继承中子类重写父类造成的可复用性变差问题。
- 类的扩展不会给已有系统引入新的错误,降低了代码出错的可能。
- 加强程序的复用性,提高了代码的可维护性、可扩展性、降低需求更变时引入的风险。
2.1 模拟场景
通过不同种类的银行卡作为场景的模拟对象,我们会使用各种类型的银行卡,如信用卡、储蓄卡,还有一些其他类型的银行卡,信用卡和储蓄卡都具备一定的消费功能,但有有一些不同,例如信用卡不宜提现,如果体现会产生高额的利息。
下面构建一个这样的模拟场景,假设在构建银行系统时,储蓄卡是第一个类,信用卡是第二个类。为了让信用卡可以使用储蓄卡的一些功能,将有信用卡继承储蓄卡,讨论是否满足里氏替换原则的一些要点。
2.2 违背原则方案
储蓄卡和信用卡都有类似的功能,但又有所不同,如都有支付、体现、还款、充值等功能,但不同点是,例如支付,储蓄卡做的是账户扣款动作,信用卡则是生成贷款单动作。下面模拟现有储蓄卡类,之后继承这个类实现信用卡的功能。
2.2.1 储蓄卡
/**
* 模拟储蓄卡功能
*/
public class CashCard {
/**
* 提现
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public String withdrawal(String orderId, BigDecimal amount){
//模拟支付成功
System.out.printf("提现成功----->单号:%s 金额:%s",orderId,amount);
return "0000";
}
/**
* 储蓄
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public String recharge(String orderId,BigDecimal amount){
//模拟充值成功
System.out.printf("储蓄成功----->单号:%s 金额:%s",orderId,amount);
return "0000";
}
}
储蓄卡主要实现了两个功能:充值、储蓄。这是模拟储蓄卡的基本功能。接下来通过继承储蓄卡实现信用卡功能。
2.2.2 信用卡
/**
* 模拟信用卡功能
*/
public class CreditCard extends CashCard {
@Override
public String withdrawal(String orderId, BigDecimal amount) {
//校验
if (amount.compareTo(new BigDecimal(1000))>0){
System.out.printf("贷款金额校验(限额1000元),单号:%s 金额:%s",orderId,amount);
return "0001";
}
//模拟生成贷款单
System.out.printf("生成贷款单,单号:%s 金额:%s",orderId,amount);
//模拟支付成功
System.out.printf("贷款成功,单号:%s 金额:%s",orderId,amount);
return "0000";
}
@Override
public String recharge(String orderId, BigDecimal amount) {
//模拟生成还款单
System.out.printf("生成还款单,单号:%s 金额:%s",orderId,amount);
//模拟还款成功
System.out.printf("还款成功,单号:%s 金额:%s",orderId,amount);
return "0000";
}
}
信用卡继承了储蓄卡后,重写了父类支付、还款方法。这种继承的有点是服用了父类的核心逻辑,但也破环了原有方法。此时继承的父类实现的信用卡类不满足里氏替换原则,也就是说此时的子类不能承担原父类的功能,直接给储蓄卡使用。
2.3 里氏替换原则改善代码
储蓄卡和信用卡在功能的使用上有些许类似,此时可以抽取出可复用的属性和核心逻辑提取出一个抽象类,由抽象类定义所有卡的公用的核心属性、逻辑,把卡的支付和还款等动作抽象成一个正向和逆向操作。
2.3.1 抽象银行卡类
/**
* 银行卡抽象类
*/
public abstract class BankCard {
private String cardNo; //卡号
private String cardDate; //开卡时间
public BankCard(String cardNo, String cardDate) {
this.cardNo = cardNo;
this.cardDate = cardDate;
}
abstract boolean rule(BigDecimal amount);
//正向入账,加钱
public String positive(String orderId, BigDecimal amount) {
//入账成功,存款、还款
System.out.printf("卡号%s 入款成功,单号:%s 金额:%s", cardNo, orderId, amount);
return "0000";
}
//逆向入账,减钱
public String negative(String orderId, BigDecimal amount) {
//入账成功,存款、还款
System.out.printf("卡号%s 出款成功,单号:%s 金额:%s", cardNo, orderId, amount);
return "0000";
}
public String getCardNo() {
return cardNo;
}
public String getCardDate() {
return cardDate;
}
}
在抽象银行卡类中,提供了基本的卡属性,包括卡号、开卡时间,以及正向入账,加钱;逆向入账减钱操作;接下来继承这个抽象类实现储蓄卡的功能逻辑。
2.3.2 储蓄卡类
/**
* 模拟储蓄卡功能
*/
public class CashCard extends BankCard {
public CashCard(String cardNo,String cardDate){
super(cardNo,cardDate);
}
//过滤规则,储蓄卡直接返回true
@Override
boolean rule(BigDecimal amount) {
return true;
}
/**
* 提现
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public String withdrawal(String orderId,BigDecimal amount){
//模拟支付成功
System.out.printf("提现成功----->单号:%s 金额:%s",orderId,amount);
return super.negative(orderId,amount);
}
/**
* 储蓄
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public String recharge(String orderId,BigDecimal amount){
//模拟充值成功
System.out.printf("储蓄成功----->单号:%s 金额:%s",orderId,amount);
return super.positive(orderId,amount);
}
/**
* 风控校验
* @param cardNo 卡号
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public boolean checkRisk(String cardNo,String orderId,BigDecimal amount){
System.out.printf("风控校验,卡号%s 单号:%s 金额:%s",cardNo,orderId,amount);
return true;
}
}
储蓄卡继承抽象银行卡BankCard类,实现核心功能包括规则过滤rule,提现withdrawal,储蓄recharge和新增的扩展方法,即风控校验checkRisk。
这样实现方式满足了里氏替换原则的基本原则,即实现了抽象类的抽象方法,有没有破环父类中的原有方法。接下来实现信用卡功能,信用卡功能可以继承储蓄卡也可以继承抽象银行卡类,无论使用哪种方式都需要遵从里氏替换原则,不可以破环父类原有的方法。
2.3.3 信用卡类
/**
* 模拟信用卡功能
*/
public class CreditCard extends CashCard {
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 load(String orderId, BigDecimal amount) {
//校验
if (!rule2(amount)){
System.out.printf("生成贷款单失败,金额超限,单号:%s 金额:%s",orderId,amount);
return "0001";
}
//模拟生成贷款单
System.out.printf("生成贷款单,单号:%s 金额:%s",orderId,amount);
//模拟支付成功
System.out.printf("贷款成功,单号:%s 金额:%s",orderId,amount);
return super.negative(orderId,amount);
}
/**
* 还款,信用卡还款
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public String repayment(String orderId, BigDecimal amount) {
//模拟生成还款单
System.out.printf("生成还款单,单号:%s 金额:%s",orderId,amount);
//模拟还款成功
System.out.printf("还款成功,单号:%s 金额:%s",orderId,amount);
return super.positive(orderId,amount);
}
}
信用卡在继承父类后,使用了公用的属性,即卡号、开卡时间,并新增了符合信用卡功能的新方法,即贷款load、还款repayment,并在两个方法中都使用了抽象类的核心功能。
另外,关于储蓄卡中的校验规则,新增了自己的rule2方法,并没有破环储蓄卡中的校验方法。
以上的实现方式都是在遵从里氏替换原则下完成的,子类随时可以替代储蓄卡类。
3 功能测试
3.1 功能测试:储蓄卡。
CashCard cashCard=new CashCard(UUID.randomUUID().toString().substring(20),"2021-09-28");
//提现
cashCard.withdrawal("100001",new BigDecimal(100));
//储蓄
cashCard.recharge("100001",new BigDecimal(100));
//测试结果
提现成功----->单号:100001 金额:100
卡号a3c-31e3eef99b40 出款成功,单号:100001 金额:100
储蓄成功----->单号:100001 金额:100
卡号a3c-31e3eef99b40 入款成功,单号:100001 金额:100
3.2 功能测试:信用卡。
CreditCard creditCard=new CreditCard(UUID.randomUUID().toString().substring(20),"2021-09-28");
//支付,贷款
creditCard.load("10001",new BigDecimal(100));
//还款
creditCard.repayment("100001",new BigDecimal(100000));
//测试结果
生成贷款单,单号:10001 金额:100
贷款成功,单号:10001 金额:100
卡号2f0-2a7c70ae4c40 出款成功,单号:10001 金额:100
生成还款单,单号:100001 金额:100000
还款成功,单号:100001 金额:100000
卡号2f0-2a7c70ae4c40 入款成功,单号:100001 金额:100000
3.3 功能测试:信用卡替换储蓄卡
CashCard creditCard=new CreditCard(UUID.randomUUID().toString().substring(20),"2021-09-28");
//提现
creditCard.withdrawal("100001",new BigDecimal(100));
//储蓄
creditCard.recharge("100001",new BigDecimal(100));
//测试结果
提现成功----->单号:100001 金额:100
卡号a70-13a07862bb8d 出款成功,单号:100001 金额:100
储蓄成功----->单号:100001 金额:100
卡号a70-13a07862bb8d 入款成功,单号:100001 金额:100
通过以上测试结果可以看出,储蓄卡功能正常,继承储蓄卡的信用卡功能也是正常的。同时,原有储蓄卡类的功能也能有信用卡类支持,即 CashCard creditCard=new CreditCard(…);
继承作为面向对象的重要特性,虽然给程序带来很大的便利性,但也引入了一些弊端,继承的方式给代码带来侵入性,可移值能力降低,类之间的耦合度较高。
里氏替换原则的目的是使用约定的方式,让使用继承后的代码具备良好的扩展性和兼容性。
在日常开发的过程中使用继承的方式并不多,有些公司的规范中也不允许出现多层继承,尤其一些核心业务的扩展。如果使用继承,就一定要遵从里氏替换原则,否则会让代码出现问题的概率变大。